๐Ÿ“„

ํ•™์Šต ๊ธฐ๋ก

INDEX


Service ์ธํ„ฐํŽ˜์ด์Šค์™€ ServiceImpl์„ ๋งŒ๋“œ๋Š” ์ด์œ  [ ์ฐธ๊ณ  ]

  1. OOP, Loose coupling โ†’ ๊ฐœ๋ฐœ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ , ์‚ฌ์šฉํ•˜๋Š” ๊ฐ์ฒด๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๋‹คํ˜•์„ฑ โ†’ ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด์•ผํ•  ๊ฒฝ์šฐ ๋‹ค๋ฅธ ๊ตฌํ˜„ ๊ฐ์ฒด(ServiceImpl2)๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉ - ์œ ์ง€๋ณด์ˆ˜ ์ธก๋ฉด์—์„œ ์ข‹๋‹ค.
  1. 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๋Š” ๋ฌธ์ œ๊ฐ€ ๋  ๋งŒํ•œ ์ƒํ™ฉ์ด ์กด์žฌํ•œ๋‹ค.
  1. @Enumerated(EnumType.ORDINAL)์€ Enum์—์„œ ์ •์˜๋œ ์ˆœ์„œ์˜ ์ธ๋ฑ์Šค๋ฅผ DB์— ์ €์žฅํ•œ๋‹ค. ๋งŒ์•ฝ Enum์ด ์ •์˜๋œ ์ˆœ์„œ๊ฐ€ ๋ฐ”๋€Œ๊ฑฐ๋‚˜, ์ค‘๊ฐ„์— ์ƒˆ๋กœ์šด ๊ฐ’์ด ๋“ค์–ด๊ฐ„๋‹ค๋ฉด ์ธ๋ฑ์Šค๊ฐ€ ๋ฐ”๋€Œ์–ด์„œ ํฐ์ผ๋‚œ๋‹ค... ๋ฌผ๋ก  ๋’ค์— ์ฐจ๋ก€๋Œ€๋กœ ์ถ”๊ฐ€ํ•˜๋ฉด ๋˜๊ฒ ์ง€๋งŒ, ์•„์˜ˆ ์œ„ํ—˜์„ ์ฐจ๋‹จํ•˜๋Š” ๋ฐฉํ–ฅ์ด ์ข‹์„๊ฑฐ๋ผ๊ณ  ์ƒ๊ฐํ•ด์„œ ORDINAL์€ ์ง€์–‘ํ•œ๋‹ค.
  1. @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.")); } ... }

 
 

์ถ”๊ฐ€ ํ•™์Šต ์˜ˆ์ •