INDEX
Service ์ธํฐํ์ด์ค์ ServiceImpl์ ๋ง๋๋ ์ด์ [ ์ฐธ๊ณ ]
- OOP, Loose coupling โ ๊ฐ๋ฐ ์ฝ๋๋ฅผ ์์ ํ์ง ์๊ณ , ์ฌ์ฉํ๋ ๊ฐ์ฒด๋ฅผ ๋ณ๊ฒฝํ ์ ์๋๋ก ํ๋ ๋คํ์ฑ โ ๋ค๋ฅธ ๊ธฐ๋ฅ์ ์ถ๊ฐํด์ผํ ๊ฒฝ์ฐ ๋ค๋ฅธ ๊ตฌํ ๊ฐ์ฒด(ServiceImpl2)๋ฅผ ๋ง๋ค์ด ์ฌ์ฉ - ์ ์ง๋ณด์ ์ธก๋ฉด์์ ์ข๋ค.
- AOP AOP์ ํธ๋์ญ์ ์ Service ์ธํฐํ์ด์ค์์ ์ฒ๋ฆฌ โ ์คํ๋ง์์ AOP๋ฅผ ๊ตฌํํ ๋ JDK์ ๊ธฐ๋ณธ ํ๋ก์๋ฅผ ์ฌ์ฉํ๋๋ฐ, ์ด ํ๋ก์๋ ์ธํฐํ์ด์ค ๊ธฐ๋ฐ์ผ๋ก ๋์
Facade ํจํด ์ ์ฉ
Facade
: ๊ฑด๋ฌผ์ ์ธ๊ด(ํ๋์ค์ด). ๊ฑด๋ฌผ์ ๋ฐ์์ ์์ ๊ตฌ์กฐ๋ฅผ ๋ณผ ์ ์๋ค.- Facade ํจํด์ ๋ง์ ์๋ธ์์คํ (๋ด๋ถ ๊ตฌ์กฐ)์ ๊ฑฐ๋ํ ํด๋์ค(์ธ๋ฒฝ)๋ก ๊ฐ์ผ๋ค.
- Facade ํจํด ์ ์ฉ ์์
- ์๋ก ๋ค๋ฅธ ํด๋์ค A, B, C ๊ฐ ์๋๋ฐ ์ด๋ฅผ ํฉ์น ํด๋์ค๊ฐ ํ์ํ๋ค. ์ด๋ A, B, C๋ฅผ ํ๋๋ก ๊ฐ๋ ํด๋์ค 'D'๋ฅผ ๋ง๋๋ ๊ฒ์ด ๋ํ์ ์ธ Facade ํจํด์ด๋ผ๊ณ ํ ์ ์๋ค.
- โญ ๋ด๊ฐ ์ง๋ฉดํ ๋ฌธ์ ์ Facade ํจํด ์ ์ฉ
- ์๋ฅผ ๋ค์ด, UserService์์ UserRepository๋ฅผ ์์ฑ์ ์ฃผ์ ๋ฐ์ ์ฌ์ฉํ๋๋ฐ ์ด๋ ๋ค๋ฅธ Repository๋ ํ์ํ๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก UserService์์๋ UserRepository๋ง ์ฌ์ฉํ๋๊ฒ ์ผ๋ฐ์ ์ด๋ผ ์ด๋ Facade ํด๋์ค๋ฅผ ๋ฐ๋ก ๋ง๋ค์๋ค. (๋ฉํ ๋์ ์กฐ์ธ) โ UserRepository์ ๋ค๋ฅธ Repository๋ฅผ ๊ฐ์ด ์ฌ์ฉํ๋ ์๋ก์ด Service ํด๋์ค๋ฅผ ์์ฑ โ ๋ค์ด๋ฐ์ ํ์๋ฅผ ๋ถ์๋ค. (์ด๋ค ํ์๋ฅผ ์ํด ๋ง๋ค์ด์ง Service)
RestDocs ์ ์ฉ [ ์ฐธ๊ณ ]
- build.gradle ํ์ผ ์ค์
plugins { id "org.asciidoctor.convert" version "1.5.9.2" // AsciiDoc ํ์ผ์ converting, Build ํด๋์ ๋ณต์ฌ } asciidoctor { dependsOn test // } bootJar { dependsOn asciidoctor // test ์คํ ํ asciidoctor ์คํ from ("$/html5") { // html ํ์ผ ์์ฑ into 'static/docs' } } dependencies { ... ์๋ต testImplementation('org.springframework.restdocs:spring-restdocs-mockmvc') // mockmvc๋ฅผ restdocs์ ์ฌ์ฉํ ์ ์๊ฒ ํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ }
- RestDocs ์ ์ฉ ์์
@AutoConfigureRestDocs @AutoConfigureMockMvc @SpringBootTest class CartControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; ... @Test @Transactional void testRestDocs() throws Exception { ... mockMvc.perform(get("/api/v1/restdocs/{Id}", id) .content(objectMapper.writeValueAsString(req)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(print()) .andDo(document("restdocs-test", // ์ ์ฅ ๋ ํ์ผ๋ช pathParameters( // path parameters parameterWithName("id").description("์์ด๋") ), requestFields( // request body fieldWithPath("firstName").type(JsonFieldType.STRING).description("์ด๋ฆ"), fieldWithPath("lastName").type(JsonFieldType.STRING).description("์ฑ"), fieldWithPath("birthDate").type(JsonFieldType.STRING).description("์๋ ์์ผ") ), responseFields( // response body fieldWithPath("statusCode").type(JsonFieldType.NUMBER).description("๊ฒฐ๊ณผ์ฝ๋"), fieldWithPath("serverDatetime").type(JsonFieldType.STRING).description("์๋ฒ์๊ฐ"), fieldWithPath("data.person.id").type(JsonFieldType.NUMBER).description("์์ด๋"), fieldWithPath("data.person.firstName").type(JsonFieldType.STRING).description("์ด๋ฆ"), fieldWithPath("data.person.lastName").type(JsonFieldType.STRING).description("์ฑ"), fieldWithPath("data.person.age").type(JsonFieldType.NUMBER).description("๋์ด"), fieldWithPath("data.person.birthDate").type(JsonFieldType.STRING).description("์๋ ์์ผ") ) )); } }
[Lombok] Test Code ์์ฑ์ @Slf4j ๊ฐ ์ธ์์ด ์๋๋ ๊ฒฝ์ฐ (Gradle)
- build.gradle ์ค์ ์ ์ถ๊ฐํด์ค์ผํ๋ค.
dependencies { ... testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' ... }
[JPA] Enum์ List๋ก ๋ฐ๊ณ ์ถ์ ๊ฒฝ์ฐ
โ ์๋ก์ด ํด๋์ค๋ฅผ ๋ง๋ค์ด์ List๋ก ๋ฐ๋๊ฒ ์ ํฉํ๋ค.
@Entity public class Accommodation { @Id @Column(name = "accommodation_id") @GeneratedValue(strategy = GenerationType.AUTO) private Long Id; @Column(name = "accommodation_name") private String accommodationName; @OneToMany(mappedBy = "accommodation", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private Set<AccommodationOption> options = new HashSet<>(); // ์๋๋ Option๋ค์ Enum์ผ๋ก ๋์ดํด์ List<>๋ก ๋ด๊ณ ์ถ์์ง๋ง ๋ถ๊ฐ๋ฅ!! // Option ํด๋์ค๋ฅผ ๋ฐ๋ก ๋ง๋ค์ด์ Set์ ๋ด์๋ค. (์ค๋ณต ์ ๊ฑฐ) } @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class AccommodationOption { @Id @Column(name = "accommodation_option_id") @GeneratedValue(strategy = GenerationType.AUTO) private Long Id; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "accommodation_id", referencedColumnName = "accommodation_id", nullable = false) private Accommodation accommodation; @Builder public AccommodationOption(final Accommodation accommodation) { this.accommodation = accommodation; accommodation.addOption(this); } }
[JPA] @Enumerated
๊ฐ์ ํ๊ธฐ [ ์ฐธ๊ณ ]
โ
@Enumerated
๋ ๋ฌธ์ ๊ฐ ๋ ๋งํ ์ํฉ์ด ์กด์ฌํ๋ค.-
@Enumerated(EnumType.ORDINAL)
์ Enum์์ ์ ์๋ ์์์ ์ธ๋ฑ์ค๋ฅผ DB์ ์ ์ฅํ๋ค. ๋ง์ฝ Enum์ด ์ ์๋ ์์๊ฐ ๋ฐ๋๊ฑฐ๋, ์ค๊ฐ์ ์๋ก์ด ๊ฐ์ด ๋ค์ด๊ฐ๋ค๋ฉด ์ธ๋ฑ์ค๊ฐ ๋ฐ๋์ด์ ํฐ์ผ๋๋ค... ๋ฌผ๋ก ๋ค์ ์ฐจ๋ก๋๋ก ์ถ๊ฐํ๋ฉด ๋๊ฒ ์ง๋ง, ์์ ์ํ์ ์ฐจ๋จํ๋ ๋ฐฉํฅ์ด ์ข์๊ฑฐ๋ผ๊ณ ์๊ฐํด์ ORDINAL์ ์ง์ํ๋ค.
@Enumerated(EnumType.STRING)
์ ๋ฌธ์์ด์ ์ ์ฅํ๊ธฐ ๋๋ฌธ์ DB ๊ณต๊ฐ ๋ญ๋น๊ฐ ๋ฐ์ํ๋ค.
์ด๋ฅผ ๊ฐ์ ํ ์ ์๋ ๊ฒ์ด Attribute Converter์ด๋ค.
@Entity public class Accommodation { @Id @Column(name = "accommodation_id") @GeneratedValue(strategy = GenerationType.AUTO) private Long Id; @Column(name = "accommodation_name") private String accommodationName; @Column(name = "region") @Convert(converter = RegionAttributeConverter.class) private Region region; } public enum Region { SEOUL("์์ธ"), GYEONGGI("๊ฒฝ๊ธฐ๋"), GANGWON("๊ฐ์๋"), CHUNGCHEONG("์ถฉ์ฒญ๋"), GYEONGSANG("๊ฒฝ์๋"), JEONLA("์ ๋ผ๋"), JAEJU("์ ์ฃผ๋"); private final String regionName; Region(final String regionName) { this.regionName = regionName; } }
@Converter public class RegionAttributeConverter implements AttributeConverter<Region, String> { @Override public String convertToDatabaseColumn(Region region) { return region.regionName; // DB์ ์ ๋ ฅ๋๋ ๋ฐ์ดํฐ } @Override public Region convertToEntityAttribute(String regionName) { return Stream.of(Region.values()) // DB์ ์ ๋ ฅ ๋์ด์๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ Enum Type์ผ๋ก ๋ณํ .filter(c -> c.regionName.equals(regionName)) .findFirst() .orElseThrow(IllegalArgumentException::new); } }
- Enum์ ์ค๋ณต ์ฝ๋๋ฅผ ์ค์ด๊ณ ์ ์ง๋ณด์์ ์ ๋ฆฌํด์ง๋ค.
[JPA] JPA Auditing [ ์ฐธ๊ณ ]
๋๋ถ๋ถ์ ์ํฐํฐ์์ ์ค๋ณต๋๋ ํ๋๋ฅผ ์ด๋
ธํ
์ด์
์ผ๋ก ๊น๋ํ๊ฒ ์ ์ฉํ ์ ์๋ค.
์คํ๋ง ํ๋ ์์ํฌ์์ ์ ๊ณตํ๋ ์ด๋
ธํ
์ด์
์ด๋ค.
@CreatedDate
: ์ํฐํฐ๋ฅผ ์์ฑํ ์๊ฐ์ ์ ์ฅ
@LastModifiedDate
: ๋ง์ง๋ง์ผ๋ก ์์ ํ ์๊ฐ์ ์ ์ฅ
@CreatedBy
: ์์ฑํ ์ฌ๋์ ์ ์ฅ
@LastModifiedBy
: ๋ง์ง๋ง์ผ๋ก ์์ ํ ์ฌ๋์ ์ ์ฅ
@CreatedBy
์ @LastModifiedBy
๋ Spring Security์ ContextHolder์์ ๋ค์ด์๋ ์ ์ ์ ๋ณด์ name๊ฐ์ผ๋ก ๋งคํํด์ค๋ค.
๋ง์ฝ Customํ๊ฒ ๊ตฌํํ๋ ค๋ฉด AuditorAware ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํด์ผ ํ๋ค.
Spring Security์ ContextHolder์ ์กด์ฌํ๋ Authentication ์ ๋ณด๋ฅผ ํตํด์ ๋งคํํ๋ค.
์ด ๋ถ๋ถ์ Spring Security๋ฅผ ๋ฐฐ์ฐ๊ณ ๋์ ์ ์ฉ์์ผ๋ด์ผ๊ฒ ๋ค! (ํ์ฌ๋ Id๋ฅผ ๋งคํ)์ฃผ๋ก BaseEntity๋ฅผ ๊ตฌํํด์ ์ํฐํฐ ํด๋์ค์์ ์์๋ฐ๋๋ฐ,
@CreatedBy
์ @LastModifiedBy
๋ ํน์ ํ ์ํฐํฐ์์๋ง ํ์ํด์ ๊ธฐ๋ฅ์ ๋ถ๋ฆฌํด ๊ตฌํํ์๋ค.- ๊ตฌํ ์์
- SpringBoot Application์
@EnableJpaAuditing
์ ์ถ๊ฐํด์ค์ผ ์ ์ฉ๋จ @MappedSuperclass
: ์ํฐํฐ๊ฐ ํด๋น ์ถ์ํด๋์ค๋ฅผ ์์ํ ๊ฒฝ์ฐ createdAt, modifiedAt ๋ฑ์ ์ปฌ๋ผ์ผ๋ก ์ธ์@EntityListeners(AuditingEntityListener.class)
: ํด๋น ํด๋์ค์ Auditing ๊ธฐ๋ฅ์ ํฌํจ
@SpringBootApplication @EnableJpaAuditing public class Application { ... }
@Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @NoArgsConstructor(access = AccessLevel.PROTECTED) public abstract class BaseEntity extends BaseTimeAndDeletedEntity { @CreatedBy @Column(name = "created_by", updatable = false) private Long createdBy; @LastModifiedBy @Column(name = "modified_by") private Long modifiedBy; }
@Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @NoArgsConstructor(access = AccessLevel.PROTECTED) public abstract class BaseTimeAndDeletedEntity { @CreatedDate @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; @LastModifiedDate @Column(name = "modified_at") private LocalDateTime modifiedAt; @Column(name = "is_deleted", columnDefinition = "boolean default false") private Boolean isDeleted = false; // Soft Delete๋ฅผ ์ ์ฉํ๋ค. public void setIsDeleted(final Boolean deleted) { this.isDeleted = deleted; } }
(+์ถ๊ฐ ๋น๊ต)
- Hibernate Annotations
@CreationTimestamp
: INSERT ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ ๋, ํ์ฌ ์๊ฐ์ ๊ฐ์ผ๋ก ์ฑ์์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๋ค.@UpdateTimestamp
: UPDATE ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ ๋, ํ์ฌ ์๊ฐ์ ๊ฐ์ผ๋ก ์ฑ์์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๋ค.
์คํ๋ง ํ๋ ์์ํฌ์์ ์ ๊ณตํ๋
@CreatedDate
, @LastModifiedDate
์ ์ฑ๋ฅ ๋ฐ ๊ธฐ๋ฅ์ ํฐ ์ฐจ์ด๋ ์์ง๋ง, ์ต๊ทผ์๋ ํ์ด๋ฒ๋ค์ดํธ ์ด๋
ธํ
์ด์
์์ฒด๋ฅผ ์ ์ ์ฌ์ฉํ์ง ์๋ ์ถ์ธ์ด๋ค.[JPA] Entity์ Builder ํจํด์ ์ ์ฉํ ๋ ์ฃผ์ํ ์ [ ์ฐธ๊ณ 1, ์ฐธ๊ณ 2 ]
Entity๋ฅผ ํจ์จ์ ์ผ๋ก ์์ฑํ๊ธฐ ์ํด ์ฃผ๋ก Builder ํจํด์ ํ์ฉํ๋ค. ์ด๋ ํด๋์ค์
@Builder
๋ฅผ ๋ถ์ด๊ธฐ๋ณด๋ค๋ ํ์ํ ํ๋๋ง์ ์ธ์๋ก ๋ฐ๋ ์์ฑ์์ @Builder
๋ฅผ ๋ถ์ด๋ ์ชฝ์ด ์ข๋ค.
Entity ํด๋์ค ์์ฒด์ @Builder
๋ฅผ ๋ถ์ด๊ฒ๋๋ฉด ์ธ์๋ก ๋ฐ์ง ๋ง์์ผํ๋ ํ๋(ex. ์ฐ๊ด ๊ด๊ณ๊ฐ ์๋ ๋ค๋ฅธ Entity)๊น์ง ๋ฃ์ด์ ๊ฐ์ฒด๊ฐ ์์ฑ๋ ์ ์๋ค.@Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class Member { @Id @Column(name = "member_id") private String id; @Column(name = "password", nullable = false) private String password; @OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private List<Review> reviews = new ArrayList<>(); @Builder // ํ์ํ ์ธ์๋ง ๋ฐ๋ Builder public Member(final String id, final String password) { this.id = id; this.password = password; } }
@Builder
๋ AllArgsConstructor๊ฐ ํ์ํ๋ค.@Builder
๋ ๊ธฐ๋ณธ ์์ฑ์๊ฐ ์ ์๋์ด ์์ง ์์๋, AllArgsConstructor๋ฅผ ์์์ ์์ฑํ๊ณ ์ฌ์ฉํ๋ค.
@Entity
๋ NoArgsConstructor๊ฐ ํ์ํ๋ค.@Builder
๋ฅผ ๋ถ์ฌ์ฃผ๋ฉด AllArgsConstructor๊ฐ ์์ฑ๋๊ธฐ ๋๋ฌธ์ NoArgsConstructor๋ ์์ฑ๋์ง ์์์@NoArgsConstructor
๋ฅผ ๋ถ์ฌ์ค๋ค.@NoArgsConstructor
๋ฅผ ๋ถ์ฌ์ฃผ๋ฉด AllArgsConstructor๊ฐ ์์ฑ๋์ง ์์์@AllArgsConstructor
๋ฅผ ๋ถ์ฌ์ค๋ค.- ๊ธฐ๋ณธ ์์ฑ์๋ฅผ ํตํ ๊ฐ์ฒด ์์ฑ์ ๋ง๊ธฐ์ํด
@NoArgsConstructor(access = AccessLevel.PROTECTED)
๋ก ์ค์ ํ๋ค.
[JPA] ์ฑ๊ธํ ์ด๋ธ ์ ๋ต(์์ ๊ด๊ณ)์์ @Builder ์ฌ์ฉ
์ด๋ฒ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ์๊ฒฌ์ด ๊ฐ๋ ธ๋ ๋ถ๋ถ์ด๋ค.
์ฑ๊ธํ
์ด๋ธ ์ ๋ต์ ์ฌ์ฉํ๋ฉด ํ๋๊ฐ๋ค์ด ๋ง์์ง๊ฒ ๋๋๋ฐ,
@SuperBuilder
๋ฅผ ์ฌ์ฉํ๋ฉด ์์ฑ์๋ฅผ ๋ฐ๋ก ๋ช
์ํด์ฃผ์ง ์์๋ ๋ผ์ ์ฝ๋๊ฐ ๊ฐ๊ฒฐํด์ง๋ค๋ ์๊ฒฌ์ด ์์๋ค.
์์ ๊ฐ์ฒด์์ ๋ถ๋ชจ ๊ฐ์ฒด์ ํ๋๊ฐ๋ ํ๋ฒ์ SuperBuilder๋ก ์ง์ ํ ์ ์๊ธฐ ๋๋ฌธ์, ํด๋์ค์ @Builder
๋ฅผ ๋ถ์ฌ ๋ชจ๋ ํ๋๋ฅผ ์
๋ ฅ๋ฐ๋ ๊ฒฝ์ฐ์ ์ฌ์ฉํ๋ฉด ์ ์ฉํ ๊ฒ ๊ฐ๋ค.
ํ์ง๋ง Entity๋ฅผ Builder๋ก ์์ฑํ ๋ ํ์ํ ํ๋๊ฐ๋ง Builder๋ก ์ง์ ํ๊ณ , ์ฐ๊ด๊ด๊ณ ๋งคํ์ Builder ์์ฑ ์์ ํฌํจ์ํค๊ธฐ ์ํด์๋ ๊ธฐ์กด์ Builder ํจํด์ด ๋ ์ ํฉํด๋ณด์ธ๋ค.@SuperBuilder
[ ์ฐธ๊ณ ]- ๋ถ๋ชจ ๊ฐ์ฒด๋ฅผ ์์๋ฐ๋ ์์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค ๋, ๋ถ๋ชจ ๊ฐ์ฒด์ ํ๋๊ฐ๋ Builder๋ก ์ง์ ํ ์ ์๊ฒ ํ๊ธฐ ์ํด ์ฌ์ฉ
// ๋ถ๋ชจ ํด๋์ค @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @SuperBuilder public class Product { @Column(name = "business_address") private String businessAddress; @Column(name = "business_name") private String businessName; }
// ์์ ํด๋์ค @Entity @DiscriminatorValue("ACCOMMODATION") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @SuperBuilder public class Accommodation extends Product { @Column(name = "accommodation_name") private String accommodationName; @Column(name = "accommodation_notice") @Lob private String accommodationNotice; }
@Builder
- ๊ธฐ์กด ๋ฐฉ์์ Builder ํจํด ์ ์ฉ
// ๋ถ๋ชจ ํด๋์ค @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class Product { @Column(name = "business_address") private String businessAddress; @Column(name = "business_name") private String businessName; }
// ์์ ํด๋์ค @Entity @DiscriminatorValue("ACCOMMODATION") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class Accommodation extends Product { @Column(name = "accommodation_name") private String accommodationName; @Column(name = "accommodation_notice") @Lob private String accommodationNotice; @Builder public Accommodation(final String businessAddress, final String businessName, final String accommodationName, final String accommodationNotice) { super(businessAddress, businessName); this.accommodationName = accommodationName; this.accommodationNotice = accommodationNotice; // ์ฐ๊ด๊ด๊ณ ํธ์ ๋ฉ์๋ ์ถ๊ฐ ๊ฐ๋ฅ } }
[JPA] @Transactional์ (readOnly=true) ์ต์ [ ์ฐธ๊ณ ]
Entity๊ฐ ์์์ฑ ์ปจํ
์คํธ์์ ๊ด๋ฆฌ๋๋ฉด 1์ฐจ ์บ์, ๋ณ๊ฒฝ๊ฐ์ง ๋ฑ ํํ์ด ๋ง์ง๋ง, ์ค๋
์ท์ ๋ณด๊ดํ๋ ๋ฑ ๋ ๋ง์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๋จ์ ์ด ์กด์ฌํ๋ค.
๋ง์ฝ ์กฐํ๋ง ํ๋ ๊ฒฝ์ฐ, ์ฝ๊ธฐ ์ ์ฉ์ผ๋ก ์ํฐํฐ๋ฅผ ์กฐํํ๋ฉด ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ์ค์ผ ์ ์๋ค. + dirty checking ์๋ต
๋ํ, ์๋์น ์๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๋ ๊ฒฝ์ฐ๋ฅผ ์๋ฐฉํ ์ ์๋ค.
์คํ๋ง ํ๋ ์์ํฌ์์ ์ ๊ณตํ๋
@Transactional
์์๋ readOnly ๋ผ๋ ์ต์
์ ์ ๊ณตํ๋ค. โ import org.springframework.transaction.annotation.Transactional;
Service ์ ์ฒด์ (readOnly=true) ์ต์
์ฌ์ฉ์ ์ ๋ํ๊ธฐ ์ํด ํด๋์ค์ @Transactional(readOnly = true)
๋ฅผ ๋ถ์ฌ์คฌ๊ณ , readOnly๊ฐ ์๋ ๋ฉ์๋์๋ง ์ถ๊ฐ๋ก @Transactional
์ ๋ถ์ฌ์คฌ๋ค.@Service @RequiredArgsConstructor @Transactional(readOnly = true) public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final UserConverter userConverter; @Override @Transactional public Long createUser(final UserCreateRequestDto dto) { return userRepository .save(userConverter.toEntity(dto)) .getUserId(); } @Override // readOnly = true public UserResponseDto findById(final Long userId) { return userRepository .findById(userId) .map(userConverter::toResponseDto) .orElseThrow(() -> new NotFoundException("User is not found.")); } ... }
์ถ๊ฐ ํ์ต ์์
- Exception Handler [ https://jeong-pro.tistory.com/195 ]
- ์ ๊ทํ ์ญ์ ๊ทํ [ https://jaenjoy.tistory.com/15 ]