(JPA+Boot)엔티티 구현
JPA 엔티티를 구현할 때는 객체 중심 설계를 기본 원칙으로 삼고, 연관관계 매핑, 지연 로딩, 값 타입 활용, 편의 메서드 구현 등을 통해 효율적인 도메인 모델을 구축할 수 있습니다.
(도메인) 테이블 설계
N:M 관계는 1:N, N:1 로 풀기
-
N:M 관계를 두 테이블 만으로 구성하는건 데이터 중복 야기.
외래키를 2개 써서 해결하려 해도 애초에 외래키 2개 사용 자체가 너무 비효율.
가능은 한지도 모르겠고.테이블 관계 TIP
- 관계를 생각 할 때 테이블로 생각하지 말고, ‘한 행’을 기준으로 생각. 테이블 명도 마찬가지.
- ‘학생’, ‘수업’
- ‘학생’, ‘수업’
- 논리적으로 생각할 땐, 연결(매핑) 테이블은 생각하지 않는다.
- 철수의 학생 코드는 학생_수업 테이블에 여러개 존재한다. (X)
- 철수는 국어, 영어, 수학 수업을 수강한다.(O)
- 철수의 학생 코드는 학생_수업 테이블에 여러개 존재한다. (X)
- 항상 일대다(1:N) 기준으로 생각하자. 다대일(N:1)보다 직관적
- 철수가 여러 수업을 수강한다.(O)
- 국어, 영어, 수학은 철수를 수용 한다.(X)
- 철수가 여러 수업을 수강한다.(O)
- 관계를 생각 할 때 테이블로 생각하지 말고, ‘한 행’을 기준으로 생각. 테이블 명도 마찬가지.
외래키가 있어야할 위치
-
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); -- 훈이 추가
- Team 테이블에 pk 중복 문제
-
1:1의 경우 상황에 따라 사용 - 보통은 주 테이블에 외래키 사용
- 주 테이블 외래키 단방향 - 단점 : 값 없으면 외래 키에 null 허용
- 대상 테이블에 외래키 양방향 - 단점 : 무조건 즉시로딩
객체 관점과 DB 관점의 양방향 차이
데이터베이스의 양방향 관계: 외래 키 하나만으로도 양쪽 테이블을 자유롭게 조인할 수 있다. 따라서 단방향이나 양방향의 개념이 특별히 존재하지 않는다.
객체의 양방향 관계: 참조 필드가 있는 쪽에서만 다른 객체를 참조할 수 있다는 특징이 있다. 따라서 두 가지 관계가 존재한다.(단방향, 양방향)@OneToMany(mappedBy = "character") // 양방향
- 이렇게 양방향을 설정해야 JPA에선 양방향 사용 가능하다.
상속 매핑은 일반적인 전략, Mapped Superclass
을 사용
-
일반적인 전략 : JOINED, SINGLE_TABLE 방식이 유명한데 보통 JOINED 방식을 선호
-
부모 자식간에 join을 하게 됨
- 참고로 join 열에 Index 추가하면 빠르게 join이 가능
- 테이블이 너무 단순하다면 SINGLE_TABLE 을 사용 -> 조인이 없어서 속도 빠름!
-
부모 자식간에 join을 하게 됨
-
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값 관리
- 자동으로 IDENTITY, SEQUENCE, TABLE 중 택1
엔티티 설계 때 연관관계는 단방향 우선 개발(테스트) 후 양방향 관계 추가
//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 왜 발생??
참고 -> User:Article = 1:N 가정
(1)즉시로딩 예시
모든 User 검색(findAll) 요청(1번) 시 User의 컬럼에 Article 때문에(N번) 추가 쿼리
즉시 로딩임을 보고 바로 추가 쿼리 날렸음
(2)지연로딩 예시
모든 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
자동 갱신)
- “임베디드 객체” 는 값 타입 하나. @Embeddable, @Embedded 로
-
-
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
선언만 해도 바로 사용 가능!
- ex:
-
주의: 객체에 관한 생성자가 1개일때 Spring 4.3이후부턴 자동으로 @Autowired 가 붙어서 위 final 방식을 사용한거지만
여러 생성자를 사용할 경우는 무슨 생성자에 생성자 주입을 사용할지 선택해서 @Autowired를 꼭 붙여줘야 함.
- 즉, DI 중 @Resource(이름기반), @Autowired(타입기반)를 이용한 Field Injection보다는
엔티티의 메서드 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 같은 변환이 가능합니다.
-
Thymeleaf 같은 템플릿 엔진을 사용해 HTML 폼을 전송할 때, Spring은 자동으로 타입 변환을 지원합니다. 예를 들어, 문자열을 숫자나 날짜로 변환하는 경우,
-
(2) HTTP API 응답 (
@ResponseBody
)-
@ResponseBody
를 사용하는 경우, HTML을 반환하는 게 아니라 데이터 (JSON, XML 등)를 반환하는 것입니다. 이때 Spring의 ConversionService는 적용되지 않습니다. - 대신, JSON 변환을 처리하기 위해
HttpMessageConverter
가 사용됩니다. 일반적으로는 Jackson 라이브러리가 Spring Boot에 포함되어 있어 객체를 JSON으로 변환해줍니다.
-
-
중요한 차이점은:
-
HTTP API 응답에서는 타입 변환은
HttpMessageConverter
가 담당하며, 자동 타입 변환(ConversionService)는 적용되지 않습니다. -
ConversionService는 주로 폼 데이터(예: 템플릿 렌더링)에서 쓰이고,
HttpMessageConverter
는 JSON 변환처럼 HTTP 메시지 본문을 처리할 때 사용됩니다.
-
HTTP API 응답에서는 타입 변환은
-
(1) 일반적인 폼 전송 (웹 애플리케이션)
-
(1) 웹 -
-
댓글남기기