(JPA+Boot)엔티티 구현

JPA 엔티티를 구현할 때는 객체 중심 설계를 기본 원칙으로 삼고, 연관관계 매핑, 지연 로딩, 값 타입 활용, 편의 메서드 구현 등을 통해 효율적인 도메인 모델을 구축할 수 있습니다.



(도메인) 테이블 설계

N:M 관계는 1:N, N:1 로 풀기

  • N:M 관계를 두 테이블 만으로 구성하는건 데이터 중복 야기.
    외래키를 2개 써서 해결하려 해도 애초에 외래키 2개 사용 자체가 너무 비효율.
    가능은 한지도 모르겠고.

    테이블 관계 TIP
    1. 관계를 생각 할 때 테이블로 생각하지 말고, ‘한 행’을 기준으로 생각. 테이블 명도 마찬가지.
      • ‘학생’, ‘수업’
    2. 논리적으로 생각할 땐, 연결(매핑) 테이블은 생각하지 않는다.
      • 철수의 학생 코드는 학생_수업 테이블에 여러개 존재한다. (X)
      • 철수는 국어, 영어, 수학 수업을 수강한다.(O)
    3. 항상 일대다(1:N) 기준으로 생각하자. 다대일(N:1)보다 직관적
      • 철수가 여러 수업을 수강한다.(O)
      • 국어, 영어, 수학은 철수를 수용 한다.(X)
        N:M관계
        1:N:1관계


외래키가 있어야할 위치

  • 1:N, N:1 의 경우 N에 사용

    1쪽에 FK 놓으면 2가지 단점


    플레이어(N) 2명 등록 후 첫 팀(1)에 모두 등록 시?? 플레이어:팀=N:1 관계

    • Team 테이블에 pk 중복 문제
    • Player 테이블 업뎃 시 Team 테이블도 같이 업뎃 문제
      insert into Player (id, name) values (1, '철수');
      insert into Player (id, name) values (2, '훈이');
      insert into Team (id, name, player_id) values (1, '떡잎팀', 1); -- 철수 추가
      insert into Team (id, name, player_id) values (1, '떡잎팀', 2); -- 훈이 추가
      
  • 1:1의 경우 상황에 따라 사용 - 보통은 주 테이블에 외래키 사용

    • 주 테이블 외래키 단방향 - 단점 : 값 없으면 외래 키에 null 허용
    • 대상 테이블에 외래키 양방향 - 단점 : 무조건 즉시로딩
    객체 관점과 DB 관점의 양방향 차이


    데이터베이스의 양방향 관계: 외래 키 하나만으로도 양쪽 테이블을 자유롭게 조인할 수 있다. 따라서 단방향이나 양방향의 개념이 특별히 존재하지 않는다.
    객체의 양방향 관계: 참조 필드가 있는 쪽에서만 다른 객체를 참조할 수 있다는 특징이 있다. 따라서 두 가지 관계가 존재한다.(단방향, 양방향)

    • @OneToMany(mappedBy = "character") // 양방향
    • 이렇게 양방향을 설정해야 JPA에선 양방향 사용 가능하다.


상속 매핑일반적인 전략, Mapped Superclass 을 사용

  • 일반적인 전략 : JOINED, SINGLE_TABLE 방식이 유명한데 보통 JOINED 방식을 선호
    • 부모 자식간에 join을 하게 됨
      • 참고로 join 열에 Index 추가하면 빠르게 join이 가능
    • 테이블이 너무 단순하다면 SINGLE_TABLE 을 사용 -> 조인이 없어서 속도 빠름!
  • Mapped Superclass 전략
    • 상속 매핑으로 선언된 클래스를 상속받게 되면 해당 상속 내용을 전부 테이블에 넣을 수 있다.
    • 자세한 사용은 “엔티티 구현” 파트 참고



(도메인) 엔티티 구현

객체 중심 설계가 원칙!

예전엔 “자바끼리 통신”을 위해 엔티티(객체)에 직렬화 Serializable 인터페이스 구현 필수지만,
현재는 Json(+xml,csv등) 직렬화를 많이 사용하다 보니 Jackson 사용 시 필요가 없다.
(Spring은 자동으로 이 직렬화를 제공 -> 예: @RestController)

@Getter @Entity @Slf4j // @Slf4j 는 log
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "MEMBER", indexes = @Index(name = "IDX_MEMBER_ID", columnList = "member_id desc")) // 인덱스 추가 법
public class Member {...}
    
@Id // pk
@GeneratedValue
@Column(name = "member_id") // db컬럼명 매핑, 참고로 nullable = false 속성은 not null
private Long id; // 엔티티에선 id 에도 보통 "테이블명 생략"
  • @Entity: 엔티티 빈 자동 등록
  • 네이밍: PK인 필드명을 id 로 쓰고 직접 테이블의 컬럼명과 매핑을 선언함 -> @Column(…)
  • 개발과정에선 @Getter, @Setter를 열어두고 나중에 리팩토링으로 @Setter 제거가 편함
    • 엔티티에서의 비지니스 메서드 구현은 Setter 제거 효과
    • Setter를 최대한 사용하지 않게끔 DTO 방식 권장 -> 컨트롤러 단에서 생각하자!
    • Setter 제거 위해 생성자 Protected 패턴 (생성 편의 메서드 static으로 선언) -> protected는 동일 패키지 까지만 허용
      • @NoArgsConstructor(access = AccessLevel.PROTECTED) 활용
      • 코드 의미대로 No Args(매개변수x) 생성자를 생성하는 코드!! (범위는 Protected)
  • GenerationType.AUTO 옵션이 기본값 (확실한 건 직접 택을 권장)
    • 자동으로 IDENTITY, SEQUENCE, TABLE 중 택1
      • IDENTITY: DB가 제공하는 auto_increment 자동증가 컬럼
      • SEQUENCE: 오라클에서 사용하던 그 시퀀스
      • TABLE: 별도의 테이블로 ID값 관리


엔티티 설계 때 연관관계는 단방향 우선 개발(테스트) 후 양방향 관계 추가

//Member.java
@OneToMany(mappedBy = "member") // 양방향 (member:lists = 1:N)
private List<Lists> lists = new ArrayList<>(); // 컬렉션은 필드에서 바로 초기화

@OneToOne(fetch = FetchType.LAZY) // 단방향(=원래방향), 지연로딩 설정
@JoinColumn(name = "profile_id") // FK 가짐
private Profile profile;
  • 양방향은 코드만으로 해결 가능해서 DB 설계에 아무런 영향을 끼치지 않음

  • 양방향TIP:

    • 연관관계 편의 메서드 + mappedBy 를 세트로 항상 같이 작성

    • 외래키는 항상 1:N 중에 N이 가지게끔

    • 옵션 중에서 cascade 사용 유무는 관계가 완전 종속일때만 사용 (연관된 데이터 연쇄적 변경 효과)

      • cascade는 영속성 전이를 하므로 연관관계 매핑과는 관계 없고, 생명주기를 같이 함

      • cascade는 주 테이블에서 사용하여 전파 시키는게 일반적인데, 1:N 양방향 사용중이면 양방향에서 전파 시키는 경우가 많음 -> 아래 예시코드를 참고

      • 두 가지 예시 코드

        Task:TaskStatus 는 1:1로써 주테이블(Task)에서 cascade 전파 함.
        Lists:Task 는 N:1로써 부테이블(Lists)에서 cascade 전파 함.

        • DB 상에서 어떤 모습인지 생각해보면 Task 에선 cascade 전파 할 필요가 없음 (Lists엔 task_id 가 없으니까)
        • 그럼, 엔티티 메모리 상에선 Lists의 tasks의 요소는 어떡하냐고?
          • Lists 새로 조회하면 DB와 동기화 하니까 상관 없어.
          • 만약 필요하다면 직접 영속성 컨텍스트 초기화 하거나, 연관관계 편의메서드로 tasks요소 삭제를 해도 좋고.
        //Task.java
        @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) // 1:1관계며 같이 존재함. (생명주기 같아야함)
        @JoinColumn(name = "task_status_id")
        private TaskStatus taskStatus;
        //
        //Lists.java
        // CascadeType.REMOVE 를 해줘야 고아객체가 안생기게 되며, Lists 삭제도 정상적으로 가능
        @OneToMany(mappedBy = "lists", cascade = CascadeType.REMOVE) // 양방향
        private List<Task> tasks = new ArrayList<>();
        //
        //Task.java
        // cacade 필요한가?? 필요없다. Lists는 Task 정보를 가지고 있지 않다. Task 가 오히려 주인(fk)이다. 즉, DB상에선 Task 삭제되든 아니든 Lists는 연관없다.
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "lists_id")
        private Lists lists;
        

        </div> </details>


JPA는 즉시 or 지연 로딩 중에서 무조건 “지연 로딩” 으로 개발 => 즉시 로딩의 N+1 문제 때문

  • 정말 잘 정리하신 분! 참고 N+1

    즉시 로딩, 지연로딩에서 N+1 왜 발생??


    참고 -> User:Article = 1:N 가정

    (1)즉시로딩 예시
    image
    image
    image
    모든 User 검색(findAll) 요청(1번) 시 User의 컬럼에 Article 때문에(N번) 추가 쿼리
    즉시 로딩임을 보고 바로 추가 쿼리 날렸음

    (2)지연로딩 예시
    image
    image
    image
    모든 User 검색(findAll) 요청(1번) 후 나중에 User의 컬럼에 Article 조회할 때(N번) 추가 쿼리 발생
    지연 로딩임을 보고 처음에 추가 쿼리를 날리지는 않지만, 이후에 User.article 접근할 때 이미 User는 select끝나서 join을 사용하지 못하고 N번 Article을 추가 select 쿼리 발생

    애초에 이 상황에 findAll에 join까지 쿼리에 사용했었으면 Article을 1번의 쿼리에 다 가져와서 N+1 문제가 없었을 것이다.
    join이 즉시로딩 같지만, 마냥 즉시로딩 사용하면 N번 추가쿼리 날려 버리니 잘 구분.

  • 코드상에서 @XToOne 은 기본이 즉시 로딩이므로 반드시 지연로딩으로 전부 변경

  • 단, “지연 로딩”N+1 문제 발생시킬 수 있다는 점을 알고가자 -> fetch join으로 해결

  • “지연 로딩”이 가능한 이유 : “프록시(가짜객체)”를 사용하기 때문


중복 코드를 줄이는 효과적인 방법들

  • 임베디드 타입(값 타입)상속-Mapped Superclass 이것 두개를 잘 활용 + 간단한 건 Enum 도 좋음 -> 중복 코드를 많이 줄임

    • 값 타입 은 좁은 범위 중복 때, 상속 부분은 넓은 범위 중복 때 사용하자
      -> 상속은 필드 뿐만아니라 메소드까지 상속 되니까!

      • 값 타입 은 엔티티 클래스에 private Long id; 와 같은 필드라고 생각!
      • 값 타입식별자가 없으므로 “엔티티와 혼동X”
        • 값 타입 컬렉션은 임베디드 보다 일대다 고아+cascade를 권장
        • 식별자 있음 (엔티티임!)
        • 값 비교에 equlas,hashCode 오버라이드는 필수
      • 상속 은 일반적으로 잘 아는 그 상속
    • 설계할때 부터 “값 타입”으로 활용될거는 따로 빼서 설계 + “상속”은 중복 보고 리팩토링 때 하던지

      임베디드 타입(값 타입)+컬렉션 엔티티(값 타입 컬렉션) 예시
      • “임베디드 객체” 는 값 타입 하나. @Embeddable, @Embedded 로
      • “값 타입 컬렉션” 은 값 타입 하나 이상. 일대다 고아+cascade 로

        임베디드 객체(값 타입 하나)
      @Embeddable //임베디드 타입(값 타입) 생성
      public class Address {
        private String street;
        private String city;
        private String zipCode;
        //
        // 반드시 equals(), hashCode()를 오버라이드 해야 합니다.
        @Override
        public boolean equals(Object o) {
          if (this==o) return true;
          if (o == null || this.getClass() != o.getClass()) return false;
          Address address = (Address) o;
          return Objects.equals(street, address.street) &&
              Objects.equals(city, address.city) &&
              Objects.equals(zipCode, address.zipCode);
        }
        //
        @Override
        public int hashCode() {
          return Objects.hash(street, city, zipCode);
        }
      }
      
      @Entity
      public class User {
        @Id @GeneratedValue
        private Long id;
        private String name;
        @Embedded //임베디드 타입(값 타입) 사용
        private Address address;
        //...
      }
      //
      //테이블 구조
      +-----------------+
      |      User       |
      +-----------------+
      | id (PK)        |
      | name           |
      | street         |
      | city           |
      | zipCode        |
      +-----------------+
      

      값 타입 컬렉션 대안 -> “영속성 전이 + 고아 객체 제거” 1:N 엔티티
      @ElementCollection 로 값 타입 컬렉션이 가능하지만 별로 추천하진 않고 “영속성 전이 + 고아 객체 제거” 1:N 엔티티를 추천!

      //임베디드로 만든 Address가 아닌 엔티티로 만들기
      @Entity
      public class Address {
        @Id @GeneratedValue
        private Long id;
        private String street;
        private String city;
        private String zipCode;
        //...
      }
      
      //Member.java
      //    @ElementCollection
      //    @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
      //    private List<Address> addressHistory = new ArrayList<>();
      //
      @OneToMany(cascade = ALL, orphanRemoval = true)
      @JoinColumn(name = "MEMBER_ID")
      private List<Address> addresses = new ArrayList<>();
      
      상속-@MappedSuperclass


      특징: 필드+메소드까지 상속

      @MappedSuperclass //생성
      public abstract class BaseEntity {
        @Id
        @GeneratedValue
        private Long id;
        private LocalDate createdAt;  // 날짜만
        private LocalDate updatedAt;
        //
        @PrePersist
        public void onPrePersist() {
          this.createdAt = LocalDate.now();  // 현재 날짜로 설정
          this.updatedAt = LocalDate.now();
        }
        @PreUpdate
        public void onPreUpdate() {
          this.updatedAt = LocalDate.now();  // 수정 날짜 갱신
        }
      }
      
      @Entity
      public class Product extends BaseEntity {
        private String name;
        private double price;
        // 다른 필드들...
      }
      @Entity
      public class Order extends BaseEntity {
        private String orderNumber;
        // 다른 필드들...
      }
      

      참고 어노테이션:

      • @PrePersist: 엔티티가 처음 저장되기 전에 실행되는 메서드를 정의. (createdAt, updatedAt 자동 설정)
      • @PreUpdate: 엔티티가 업데이트되기 전에 실행되는 메서드를 정의. (updatedAt 자동 갱신)
  • ENUM 데이터 사용 시 @Enumerated(EnumType.STRING) 로 꼭 STRING 으로 옵션

    • public enum OrderStatus {
        NEW, PROCESSING, COMPLETED, CANCELLED
      }
          
      @Entity
      public class Order {
        @Enumerated(EnumType.STRING)
        private OrderStatus status;
        // 다른 필드들...
      }
      
    • 데이터베이스에 NEW, PROCESSING 같은 “문자열이 저장” 굿! (인덱스 이런게 아니라)

    • 따라서 Enum 순서가 바뀌거나 값이 바뀌어도 문제가 생기지 않음.


초기화 - 생성자 주입, 컬렉션

  • 컬렉션(List같은)은 필드에서 초기화 하자
    • 코드간결, null 문제에서 안전
    • ex: private List<Task> tasks = new ArrayList<>();
  • 의존성 주입(DI)은 Field 주입이나 setter 주입 대신에 생성자 주입을 사용하자.
    • 즉, DI 중 @Resource(이름기반), @Autowired(타입기반)를 이용한 Field Injection보다는
      @RequiredArgsConstructor와 final을 이용한 Constructor Injection을 사용하자
      • setter 주입 예시: XML빈에 property사용시 자동setter주입 or java에서 setter 사용
      • 헷갈리는 Autowired, Qualifier, Resource: @Autowired와 함께 @Qualifier를 사용하고, @Resource는 @Autowired와 @Qualifier를 한번에 대체
    • @RequiredArgsConstructor는 “final 붙은 필드를 인자로 받는 생성자”를 자동 생성
      • ex: private final ExpService expService 선언만 해도 바로 사용 가능!
    • 주의: 객체에 관한 생성자가 1개일때 Spring 4.3이후부턴 자동으로 @Autowired 가 붙어서 위 final 방식을 사용한거지만
      여러 생성자를 사용할 경우는 무슨 생성자에 생성자 주입을 사용할지 선택해서 @Autowired를 꼭 붙여줘야 함.


엔티티의 메서드 TIP : 생성_편의 메서드, 연관관계_편의 메서드, 비지니스_편의 메서드(업뎃,조회 등) 권장

  • 생성_편의 메서드 사용 권장 -> 생성자 대용 굿

    • ex: public static Member createMember() 같은 것

    • 목적 : 엔티티에 있는 다양한 속성들을 이 생성메서드 하나로 간편히 다 적용 & 무분별한 엔티티 생성을 막기 위함 -> 생성자 Protected 패턴!!

    • 이를 위해 @NoArgsConstructor(access = AccessLevel.PROTECTED) 사용!

      • 기본 생성자의 접근 제어를 PROTECTED로 설정해놓게 되면 무분별한 객체 생성에 ide 상에서 한번 더 체크할 수 있는 수단
      • Protected 접근자는 반드시 같은 패키지만 허용!! - 상위, 하위 패키지 전부 불가
      생성 메서드 코드
      // 기본생성자 Public->Protected
      @NoArgsConstructor(access = AccessLevel.PROTECTED)
      public class Order {
        // ...
        //==생성 메서드==// => 수많은 정보 한번에! 그리고 public static 필수
        public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
          Order order = new Order();
          order.setMember(member);
          order.setDelivery(delivery);
          for(OrderItem orderItem : orderItems) {
            order.addOrderItem(orderItem); //연관관계_편의 메서드 적용
          }
          order.setStatus(OrderStatus.ORDER);
          order.setOrderDate(LocalDateTime.now());
          return order;
        }
      }
      // main 함수 내부 가정
      // 외부 가능
      Order order1 = Order.createOrder(member, delivery, orderItems); 
      order1.setDelivery(delivery2); // 정상
      // 외부 불가능 (Protected:같은 패키지만 가능!!)
      Order order2 = new Order(); 
      order2.setDelivery(delivery2); // ide상에서 에러 발생
      
  • 연관관계_편의 메서드 사용 권장 -> 양방향에 굿!!

    • ex: addLists(), sestCharacter() 같은 것

      • (가정) Order와 OrderItem는 1:N이고 외래키 가진 주인은 OrderItem 인 상태이다.
      • (1) 주인인 OrderItem가 Order를 접근하는건 단방향이므로 바로 접근 가능
      • (2) Order가 OrderItem를 접근하는건 양방향이므로 조금 돌아가서 접근 가능
      • (3) Order->OrderItem 접근을 간단히 사용하기 위해 연관관계 메서드를 사용
      연관관계 메서드 코드
      // (1) OrderItem->Order 접근 예시 => 단반향
      orderItem.getOrder(); // orderItem에서 order정보 접근 모습
      //
      // (2) Order->OrderItem 접근 예시 => 반대방향 (양방향)
      order.getOrderItems().add(orderItem); // order에 orderItem추가 목적
      orderItem.setOrder(order); // 두줄 필요 (중요!!)
      //
      // (3) 연관관계 편의 메서드
      // 즉, Order->OrderItem 접근(양방향)을 편의 메서드 만들어서 활용!
      // Order 클래스 내부
      public void addOrderItem(OrderItem orderItem) {
        this.orderItems.add(orderItem);
        orderItem.setOrder(this);
      }
      // main 함수 내부
      public void main~~(){
        order.addOrderItem(orderItem); // 한줄로 order에 orderItem정보 추가
      }
      
    • 비지니스_편의 메서드 사용 권장

      • Service 파트가 아닌 Entity파트에서 비지니스 로직 구현이 가능할 것 같은 경우에는 Entity에서 개발을 적극 권장(=도메인 모델 패턴) => 장점 : 좀 더 객체 지향적인 코드

        비지니스 메서드 코드
        // 간단히 엔티티에서 구현 가능하면, 서비스가 아닌 엔티티에서 로직 구현(객체지향적)
        // 재고 추가 함수 (비지니스 로직)
        public void addStock(int quantity) {
          this.stockQuantity += quantity;
        }
        // 날짜 비교 함수 (비지니스 로직)
        private static boolean compareDate(Task task, LocalDateTime listsDate) {
          // 년,월,일 만 비교하면 충분 하므로 Time 은 비교X
          LocalDate taskDay = task.getStartTime().toLocalDate();
          LocalDate listsDay = listsDate.toLocalDate();
          if (taskDay.compareTo(listsDay) == 0) { // 동일시 0
            return true;
          }
          return false;
        }
        // update (비지니스 로직)
        public Lists updateTime(Long timerAllUseTime, Long curTime) {
          this.timerAllUseTime = timerAllUseTime;
          this.curTime = curTime;
          return this;
        }
        // 주문상품 전체 가격 조회 (비지니스 로직)
        public int getTotalPrice() {
          return getOrderPrice() * getCount(); // 가격 * 수량 = 주문상품 가격
        }
        


엔티티에 정규식(Valid)과 DTO와 타입컨버터

  • DTO는 컨트롤러단 개발하다보면 고려하게 될 거다. (컨트롤러 파트 참고)

    • DTO에 보통 Valid 적용하면 된다.

      Item.java -> dto/AddItemDto.java, dto/UpdateItemDto.java 예시 코드


      Item.java

      @Entity
      @Getter
      @NoArgsConstructor(access = AccessLevel.PROTECTED)
      public class Item {
        @Id
        @GeneratedValue
        @Column(name = "item_id")
        private Long id;
        private Long No;
        private String nickName;
        private String password;
        private String title;
        private String content;
        private String imgSrc;
        @DateTimeFormat(pattern = "yy.MM.dd.HH:mm")
        private LocalDateTime date1;
        @DateTimeFormat(pattern = "yy년 MM월 dd일 HH시 mm분")
        private LocalDateTime date2;
        //==생성 편의 메서드==//
        public static Item createItem(AddItemDto addItemDto) {
          Item item = new Item();
          item.nickName = (addItemDto.getNickName().equals("")) ? "익명" : addItemDto.getNickName();
          item.password = (addItemDto.getPassword().equals("")) ? "" : addItemDto.getPassword();
          item.title = (addItemDto.getTitle().equals("")) ? "무제" : addItemDto.getTitle();
          item.content = (addItemDto.getContent().equals("")) ? "" : addItemDto.getContent();
          item.imgSrc = addItemDto.getImgSrc();
          item.date1 = LocalDateTime.now();
          item.date2 = LocalDateTime.now();
          return item;
        }
        //==비지니스 로직 편의 메서드==//
        public Item updateItem(UpdateItemDto dto) {
          this.nickName = (dto.getNickName().equals("")) ? "익명" : dto.getNickName();
          this.password = (dto.getPassword().equals("")) ? "" : dto.getPassword();
          this.title = (dto.getTitle().equals("")) ? "무제" : dto.getTitle();
          this.content = (dto.getContent().equals("")) ? "" : dto.getContent();
          // 최신 업데이트 시간
          this.date1 = LocalDateTime.now();
          this.date2 = LocalDateTime.now();
          return this;
        }
      }
      

      AddItemDto.java -> id와 date가 없는 detail

      @Getter
      public class AddItemDto {
        @NotNull
        private String nickName;
        @NotNull
        @Pattern(regexp = "^[0-9]+", message = "비밀번호는 숫자로 입력 해주세요.")
        private String password;
        @NotNull
        private String title;
        @NotNull
        private String content;
        @NotBlank(message = "이미지가 없습니다. 다시 시도하세요.")
        private String imgSrc;
        //==생성 편의 메서드==//
        public AddItemDto(String nickName, String password, String title, String content, String imgSrc) {
          this.nickName = nickName;
          this.password = password;
          this.title = title;
          this.content = content;
          this.imgSrc = imgSrc;
        }
      }
      

      UpdateItemDto.java -> id가 있는 datil

      @Getter
      public class UpdateItemDto {
        @NotNull
        private Long id;
        @NotNull
        private String nickName;
        @NotNull
        @Pattern(regexp = "^[0-9]+", message = "비밀번호는 숫자로 입력 해주세요.")
        private String password;
        @NotNull
        private String title;
        @NotNull
        private String content;
        @NotBlank(message = "이미지가 없습니다. 다시 시도하세요.")
        private String imgSrc;
        //==생성 편의 메서드==//
        public UpdateItemDto(Long id, String nickName, String password, String title, String content,
            String imgSrc) {
          this.id = id;
          this.nickName = nickName;
          this.password = password;
          this.title = title;
          this.content = content;
          this.imgSrc = imgSrc;
        }
      }
      
  • Valid도 리팩토링때 고려하게 될 거다. (검증 파트 참고)

    • 엔티티에 적용한 예시 코드: @NotNull @Pattern(regexp = "^[0-9]+", message = "비밀번호는 숫자로 입력 해주세요.")
  • 타입컨버터 사용으로 데이터가 나중에 사용할 때 정해둔 pattern 방식으로 LocalDateTime->String 반환되는 것!

    • 예로 @DateTimeFormat, @NumberFormat 있다.

      타입컨버터 예시 코드
      @DateTimeFormat(pattern = "yy.MM.dd.HH:mm")
      private LocalDateTime date1;
      @DateTimeFormat(pattern = "yy년 MM월 dd일 HH시 mm분")
      private LocalDateTime date2;
      

      만약 타입컨버터 사용 안했으면 직접 복잡하게 구현해야 한다.

      private String date1; // string으로 변경 및 format 활용
      private String date2;
      DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("yy.MM.dd.HH:mm");
      DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yy년 MM월 dd일 HH시 mm분");
      item.date1 = LocalDateTime.now().format(formatter1);
      item.date2 = LocalDateTime.now().format(formatter2);
      
      (보충) 타입 컨버터
      • (1) 웹 - @Requestparam, @ModelAttribute, @PathVariable 스프링이 기본 지원
        • 예로 @PathVariable Long itemId 는 자동으로 String->Long 타입변환
        • “확장 가능” 하고, “애노테이션“을 제공
          • @DateTimeFormat예시 : DB엔 LocalDateTime타입, Thymeleaf는 지정한 pattern 사용
          • 예로) th:field=”*{{date1}}” 이런식으로 사용
          @Data
          static class Form {
              @NumberFormat(pattern = "###,###") // 타입 컨버터
              private Integer number;
              @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
              private LocalDateTime localDateTime; 
              // db엔 LocalDateTime 형태로 저장
              // Thymeleaf에선 지정한 "패턴"으로 출력
          }
          
      • (2) HTTP API (@ResponseBody 등) - 의 경우 지원하지 않는다(HttpMessageConverter 는 “컨버전 서비스 적용 불가”)
        • 이 경우에는 Jackson 같은 라이브러리에서 포맷터를 찾아 사용
        • JSON->객체, 객체->JSON 등등 쉽게 타입 변환 가능
      • 자세히 정리하자면?
        • (1) 일반적인 폼 전송 (웹 애플리케이션)
          • Thymeleaf 같은 템플릿 엔진을 사용해 HTML 폼을 전송할 때, Spring은 자동으로 타입 변환을 지원합니다. 예를 들어, 문자열을 숫자날짜로 변환하는 경우, @RequestParam, @ModelAttribute, @PathVariable 등의 애노테이션을 사용하여 자동 타입 변환이 됩니다.
          • 이때 Spring의 ConversionService를 사용하여 String -> Integer 또는 String -> LocalDate 같은 변환이 가능합니다.
        • (2) HTTP API 응답 (@ResponseBody)
          • @ResponseBody를 사용하는 경우, HTML을 반환하는 게 아니라 데이터 (JSON, XML 등)를 반환하는 것입니다. 이때 Spring의 ConversionService는 적용되지 않습니다.
          • 대신, JSON 변환을 처리하기 위해 HttpMessageConverter가 사용됩니다. 일반적으로는 Jackson 라이브러리가 Spring Boot에 포함되어 있어 객체를 JSON으로 변환해줍니다.
        • 중요한 차이점은:
          • HTTP API 응답에서는 타입 변환은 HttpMessageConverter가 담당하며, 자동 타입 변환(ConversionService)는 적용되지 않습니다.
          • ConversionService는 주로 폼 데이터(예: 템플릿 렌더링)에서 쓰이고, HttpMessageConverter는 JSON 변환처럼 HTTP 메시지 본문을 처리할 때 사용됩니다.



댓글남기기