반응형

orphanRemoval

  • JPA 2.0 이상에서 지원하는 것으로 ORM 스팩, JPA 레벨에서의 정의입니다.
  • orphanRemoval은 @OneToMayn 연관에서 부모 엔티티의 컬렉션 등에서 자식 엔티티가 삭제될 때 참조가 끊어지므로 DB 레벨에서도 삭제되고 @OneToOne 연관에서 엔티티가 삭제될 때 연관된 엔티티 참조가 끊어지므로 DB에서 삭제된다. 즉 참조, 연결이 끊어진(Disconnected된) 엔티티를 같이 삭제하라는 의미로 Owner 객체와 참조가 끊어진 객체들을 정리할 때 유용하다.

ex)

@Entity
class Team{
    @OneToMany(orphanRemoval=true)
    private List<Member> members;
}
반응형

'개발 > JPA' 카테고리의 다른 글

[Spring] NativeQuery  (0) 2023.09.10
[JPA] Cascade  (1) 2023.09.10
[JPA] @OneToMany 단방향 매핑의 단점  (0) 2023.09.10
JPA 무한 재귀 해결방법  (0) 2023.09.10
JPA 주요속성(@JoinColumn, @MaynToOne, @OneToMany)  (0) 2023.09.10
반응형

단점

  1. 엔티티가 관리하는 외래 키가 다른 테이블에 있음 -> 작업한 Entity가 아닌 다른 Entity에서 쿼리문이 나가는 경우가 있어 헷갈림
  2. 불필요한 쿼리문이 발생(update 등..)
  3. join table 문제

객체 저장시 update 쿼리문 추가 발생

  • Team(1) : Member(N) 구조를 가지는 테이블로 비교하겠습니다.
  • Team Entity에서 @OneToMany로 단방향을 가지는 구조입니다.

ex) Entity

ex) 2개의 member를 저장하는 코드

-> 실행 결과

member에 팀을 세팅해주는 2번의 update 쿼리가 추가로 발생함
만약 @ManyToOnE의 관계였다면 member 객체가 team을 가지고 있는 구조였다면 발생하지 않습니다.(외래키를 member가 직접 관리하기 때문에)

ex) team에 속한 member 삭제

members에서 member를 제거 했으니 해당 member에 team_id를 null로 만드는 update쿼리가 날라가는게 당연하고 memberRepository로 member를 제거했으니 delete 쿼리가 날라가는게 당연합니다.

@OneToMany 양방향

-> 위의 내용 수행 결과

ex) Team의 @OneToMany에 orphanRemoval과 cascade설정을 다음과 같이 해줍니다.

members에서 member를 삭제해주기만해도 memberRepository와 연동되어 자동으로 member를 삭제해줍니다.

반응형

'개발 > JPA' 카테고리의 다른 글

[JPA] Cascade  (1) 2023.09.10
[JPA] @OneToMany orphanRemoval  (0) 2023.09.10
JPA 무한 재귀 해결방법  (0) 2023.09.10
JPA 주요속성(@JoinColumn, @MaynToOne, @OneToMany)  (0) 2023.09.10
JPA 지연로딩(LAZY) 즉시로딩(EAGER)  (0) 2023.09.10
반응형

@JsonManagedReference와 @JsonBackReference 어노테이션 사용

  • Jackson 2.0 버전 이전에 순환 참조를 해결하기 위해 사용했던 어노테이션입니다.
  • @JsonManagedReference
    • 양방향 관계에서 정방향 참조할 변수에 어노테이션을 추가하면 직렬화에 포함된다.
  • @JsonBackReference
    • 양방향 관계에서 역방향 참조로 어노테이션을 추가하면 직렬화에서 제외된다.

Customer 객체에서 Order 객체에 @JsonManagedReference를 추가하고 Order에서는 Customer 객체에 @JsonBackReference 어노테이션을 추가하여 직렬화에서 Customer 객체를 제외 시켰습니다.

@Setter
@Getter
@ToString
public class Customer {
    private int id;
    private String name;
    @JsonManagedReference //serialized될 때 포함됨
    private Order order;
}

@Setter
@Getter
@ToString(exclude = "customer”) //toString() 실행시에도 무한 재귀가 발생하여 제외시킨다
public class Order {
    private int orderId;
    private List<Integer> itemIds;
    @JsonBackReference //serialization에서 제외된다
    private Customer customer;
}

@Test
public void infinite_recursion_해결책_JsonManagedReference_JsonBackReference() throws JsonProcessingException {
    Order order = new Order();
    order.setOrderId(1);
    order.setItemIds(List.of(10, 30));

    Customer customer = new Customer();
    customer.setId(2);
    customer.setName("Frank");
    customer.setOrder(order);
    order.setCustomer(customer);

    log.info("customer(toString) : {}", customer);
    log.info("customer(serialized json) : {}", objectMapper.writeValueAsString(customer));
    log.info("order(serialized json) : {}", objectMapper.writeValueAsString(order)); //customer정보는 제외된다
}

실행 화면

@JsonBackReference 어노테이션 선언으로 Order 객체에서 Customer 객체에 대한 정보는 빠지게 됩니다.

@JsonIdentityInfo - 추천 방식

  • Jackson 2.0 이후부터 새롭게 추가된 어노테이션입니다. @JsonIdentityInfo 어노테이션을 추가해서 직렬화에 포함 시킬 속성 값을 'property' 속성에 지정합니다.
  • @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class)
    • generator = ObjectIdGenerators.PropertyGenerator.class 클래스는 순환 참조시 사용할 Id를 생성하는데 사용되는 클래스이다.
  • @JsonIdentityInfo(property="id")
    • property 속성은 해당 클래스의 속성 이름을 지정한다.
    • 예제에서는 id는 Customer#id를 가리키고 직렬화/역직렬화 할 때 Order#customer의 역참조로 사용된다.

@JsonIgnore

제일 간단하게 해결할 수 있는 방법은 직렬화 할 때 순환 참조 되는 속성에 @JsonIgnore 어노테이션을 추가하여 직렬화에서 제외시키는 방법입니다.

@JsonIgnore
private Customer customer;

해당 customer 필드는 직렬화에서 제외되어 json 형태에 표시되지 않습니다.

반응형
반응형

@JoinColumn

  • 외래 키를 매핑할 때 사용한다.
속성 설명 기본 값
name 해당 Entity에서 매핑할 외래 키 이름 필드명 +_+ 참조하는 테이블의 기본 키 컬럼명
referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본 키 컬럼명
foreignKey(DDL) 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다.  
unique, nullable, insertable, updatable, columnDefinition, table @Column의 속성과 같다.  

@ManyToOne

  • 다대일 관계 매핑
속성 설명 기본 값
optional false로 설정하면 연관된 엔티티가 항상 있어야 한다. true
fetch 글로벌 패치 전략을 설정한다. @ManyToOne=FetchType.EAGER, @OneToMany=FetchType.LAZY
cascade 속성 전이 기능을 사용한다.  
targetEntity 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.  

@OneToMany

  • 다대일 관계 매핑
속성 설명 기본 값
mappedBy 연관관계의 주인 필드를 선택한다.  
fetch 글로벌 패치 전략을 설정한다. @ManyToOne=FetchType.EAGER, @OneToMany=FetchType.LAZY
cascade 속성 전이 기능을 사용한다.  
targetEntity 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.  
반응형
반응형

지연 로딩(LAZY)

내부 메커니즘은 위의 그림과 같다.
로딩되는 시점에 Lazy 로딩 설정이 되어있는 Team 엔티티는 프록시 객체로 가져온다.
후에 실제 객체를 사용하는 시점에(Team을 사용하는 시점에) 초기화가 된다. DB에 쿼리가 나간다.
- getTeam()으로 Team을 조회하면 프록시 객체가 조회가 된다.
- getTeam().getXXX()으로 팀의 필드에 접근할 때 쿼리가 나간다.

대부분 비즈니스 로직에서 Member와 Team을 같이 사용한다면?

  • 이런 경우 LAZY 로딩을 사용한다면 SELECT 쿼리가 따로따로 2번 나간다.
  • 네트워크를 2번 타서 조회가 이루어 진다는 이야기이므로 손해다.
  • 이때는 JPQL의 fetch join을 통해서 해당 시점에 한방 쿼리로 가져와서 쓰도록 한다.

즉시 로딩(EAGER)

fetch 타입을 EAGER로 설정하면 된다. (@ManyToOne, @OneToOne... @XXXToOne은 기본이 EAGER)
대부분의 JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회하려고 한다.
이렇게 하면 실제 조회할 때 한방 쿼리로 다 조회해온다.
실행 결과를 보면 Team 객체도 프록시 객체가 아니라 실제 객체이다.

프록시와 즉시 로딩 주의할 점

  • 실무에서는 가급적 지연 로딩만 사용한다.
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
    • @ManyToOne이 5개 있는데 전부 EAGER로 설정되어 있다고 해보자.
      • 조인이 5개 일어난다. 실무에선 테이블이 더 많다
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다
    • 실무에서 복잡한 쿼리를 많이 풀어내기 위해서 JPQL을 많이 사용한다.
    • EAGER는 반환하는 시점에 다 조회가 되어 있어야 한다. 따라서 Member를 다 가져오고 나서 그 Member와 연관된 Team을 다시 다 가져온다.
    • ex) 멤버가 2명이고 팀도 2 개다. 각각 다른 팀이다.
Team team1 = new Team();
team1.setName("teamA");
em.persist(team1);
​
Team team2 = new Team();
team2.setName("teamB");
em.persist(team2);
​
Member member1 = new Member();
member1.setUsername("memberA");
em.persist(member1);
member1.changeTeam(team1);
​
Member member2 = new Member();
member2.setUsername("memberB");
em.persist(member2);
member2.changeTeam(team2);
​
em.flush();
em.clear();
​
List<Member> members = em
                .createQuery("select m from Member m", Member.class)
  .getResultList();
​
tx.commit();

실행 결과를 보면

  1. 멤버를 조회해서 가져온다
  2. Member들의 Team이 채워주기 위해 TEAM을 각각 쿼리 날려서 가져온다.

여기서 N+1의 문제가 나온다.
-> 쿼리 1개를 날렸는데 그것 때문에 추가 쿼리가 N개 나간다는 의미이다.

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.id as id1_4_,
            member0_.createdBy as createdB2_4_,
            member0_.createdDate as createdD3_4_,
            member0_.lastModifiedBy as lastModi4_4_,
            member0_.lastModifiedDate as lastModi5_4_,
            member0_.age as age6_4_,
            member0_.description as descript7_4_,
            member0_.locker_id as locker_10_4_,
            member0_.roleType as roleType8_4_,
            member0_.team_id as team_id11_4_,
            member0_.name as name9_4_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.id as id1_8_0_,
        team0_.createdBy as createdB2_8_0_,
        team0_.createdDate as createdD3_8_0_,
        team0_.lastModifiedBy as lastModi4_8_0_,
        team0_.lastModifiedDate as lastModi5_8_0_,
        team0_.name as name6_8_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
Hibernate: 
    select
        team0_.id as id1_8_0_,
        team0_.createdBy as createdB2_8_0_,
        team0_.createdDate as createdD3_8_0_,
        team0_.lastModifiedBy as lastModi4_8_0_,
        team0_.lastModifiedDate as lastModi5_8_0_,
        team0_.name as name6_8_0_ 
    from
        Team team0_ 
    where
        team0_.id=?

결론

  • 실무에서는 가급적 LAZY 로딩 전략을 사용하자.
  • 실무에서 대부분 MEMBER와 TEAM을 함께 사용한다면 JPQL의 fetch join을 통해서 해당 시점에 한방 쿼리로 가져와서 사용하라.
  • @ManyToOne, @OneToOne과 같이 @XXXToOne 어노테이션들의 기본이 즉시 로딩(EAGER)이다.
  • @OneToMany, @ManyToMany는 기본이 지연 로딩(LAZY)이다.
반응형
반응형

1. Entity

- DB의 테이블과 매칭 되는 개념

ex) DB 구조
CREATE TABLE Student (
s_number BIGINT(20) NOT NULL AUTO_INCREMENT,
name varchar(255) not null,
);

ex) Entity 구조

@Entity
public class Student{

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(nullable = false)
    private long sNumber;

    @Column(nullable = false)
    private String name;

    // getter, setter...

}

2. EntityManager

Entity를 관리하는 역할을 수행하는 클래스이다.
엔티티 매니저 내부에 영속성 컨텍스트(Persistence Context)라는 걸 두어서 엔티티들을 관리한다.

영속성 컨텍스트(Persistence Context)

영속성: 비휘발성
컨텍스트: 하나의 환경, 공간으로 이해

영속성 컨텍스트: 영속성 컨텍스트를 관리하는 모든 EntityManager가 초기화 및 종료되지 않는 한 Entity를 영구히 저장하는 환경

EntityManager와 영속성 컨텍스트의 Entity 관리

public class Join{

    public void save(String name){
        Student s = new Student();
        s.setName(name);

        // 엔티티 매니저가 있다고 가정.
        EntityManager em;
        EntityTransaction tx = em.getTransaction();

        try{
            // 엔티티 매니저에서 수행하는 모든 로직은 트랜잭션 안에서 수행되야 한다.
            tx.begin();

            // 이렇게 하면 해당 엔티티 매니저의 영속성 컨텍스트에 위에서 만든 student 객체가 저장된다.
            // 이제 student 엔티티는 엔티티 매니저의 관리 대상이 되고, 영속성을 가졌다고 말할 수 있다.
            em.persist(s);

            // 트랜잭션을 커밋한다.
            tx.commit();
        }catch(Exception e){
            // 오류가 났다면 트랜잭션을 롤백 시켜줘야한다.
            tx.rollback();
        }finally{
            // 더 이상 사용하지 않는 자원이므로 엔티티 매니저를 종료시켜줘야 한다.
            em.close();
        }

    }
}

트랜잭션(Transaction)

  • 하나의 작업 단위
    ex) 상품 결제
  1. 상품 재고 조회
  2. 사용자의 잔고 조회
  3. 상품 재고 -
  4. 사용자 잔고에서 해당 금액을 뺌
  5. 주문 완료

위 5가지 작업중 하나라도 오류가 발생하면 전체가 오류라고 보고 맨 처음 상태로 돌려야한다.
이렇게 오류가 났을 때 처음 상태로 돌아가는(rollback) 작업의 단위를 트랜잭션이라 한다.
트랜잭션이 모두 정상 수행됬을 때는 commit을 수행해서 작업 내용을 실제 DB와 엔티티 매니저에 반영한다.

쓰기 지연 SQL 저장소

  • 영속성 컨텍스트 안에는 쓰기 지연 SQL 저장소라는 공간이 따로 존재한다.
public class Join{
    public void save(String name){

        Student s1 = new Student();
        s.setName(name);

        Student s2 = new Student();
        s.setName(name);

        // 엔티티 매니저가 있다고 가정
        EntityManager em;
        EntityTransaction tx = em.getTransaction();

        try{
            // 엔티티 매니저에서 수행하는 모든 로직은 트랜잭션 안에서 수행되야 한다.
            tx.begin();

            //쿼리는 전송되지 않는다.
            em.persist(s1);
            em.persist(s2);

            // 커밋하는 시점에 쿼리가 전송된다.
            tx.commit();
        }catch(Exception e){
            // 오류가 났다면 트랜잭션을 롤백 시켜줘야한다.
            tx.rollback();
        }finally{
            // 더 이상 사용하지 않는 자원이므로 엔티티 매니저를 종료시켜줘야 한다.
            em.close();
        }

    }
}

트랜잭션을 사용하지 않는다고 가정하고코드 동작을 살펴보자.

  1. 쿼리가 두 번 날아갈 것이다.
  2. 만약에 s2를 저장하는 시점에서 롤백을 해야할 때는 날리지 않아도 될 s1 에 대한 삽입 쿼리를 날리게 된 격이다.

하지만 쓰기 지연 SQL 저장소 및 트랜잭션에 의해 아래와 같이 동작한다.

  1. 트랜잭션이 커밋 되기 직전까지 모든 쿼리문은 영속성 컨텍스트 내부의 쓰기 지연 SQL 저장소에 저장된다.
  2. 트랜잭션이 커밋되는 순간 모든 쿼리가 한 방에 날아간다.
  3. 만약 트랜잭션 내부에서 오류가 나서 롤백을 해야한다면 애초에 날리지도 않을 쿼리를 날리지도 않는다.

3. EntityManagerFactory

  • 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안된다.
  • 동시성(Concurrency):
    사용자가 체감하기에는 동시에 수행하는 거처럼 보이지만 사실 사용자가 체감할 수 없는 짧은 시간단위로 작업들을 번갈아가면서 수행되는 것이다.
    ex) 각 스레드들이 동시에 동작하는 거 같지만 알고 보면 스레드들이 아주 짧은 시간마다 번갈아가면서 작업을 수행하고 있는 것이다.
  • 병렬(Parallelism)
    우리가 생각하는 진짜 동시에 실행하는 개념을 생각하면 된다.
    실제로 동시에 여러 작업이 수행되는 개념
    각 스레드들이 동시에 동작한다.

내가 데이터를 수정하고 있는데 다른 스레드에서 해당 데이터를 미리 수정해버리면 안되기 때문에
엔티티 매니저는 하나를 공유하면 안 되고, 상황에 따라 계속 만들어줘야한다.
엔티티 매니저를 만드는 것이 엔티티 매니저 팩토리이다.

public class Join{
    public void save(String name){

        Student s1 = new Student();
        s1.setName(name);

        // META-INF/persistence.xml에서 이름이 db인 persistence-unit을 찾아서 엔티티 매니저 팩토리를 생성
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("db");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        try{
            // 엔티티 매니저에서 수행하는 모든 로직은 트랜잭션 안에서 수행되야 한다.
            tx.begin();

            // 이렇게 하면 해당 엔티티 매니저의 영속성 컨텍스트 위에서 만든 student 객체가 저장된다.
            // 이제 student 엔티티 매니저의 관리 대상이 되고, 영속성을 가졌다고 말할 수 있다.
            em.persist(s1);

            // 트랜잭션을 커밋한다.
            tx.commit();

        }catch(Exception e){
            // 오류가 났다면 트랜잭션 롤백 수행
            tx.rollback();
        }fianlly{
            // 더 이상 사용하지 않는 자원이므로 엔티티 매니저를 종료시켜줘야 한다.
            em.close();
        }

    }
}

Entity Manager Factory에서 제품(Entity Manager)를 찍어내는 개념
Entity Manager Factory는 Entity Manager와 달리 여러 스레드가 동시에 접근해도 안전하다.
단순히 Entity Manager만 찍기 때문

Entity Manager Factory를 짓는 비용은 크다
따라서 Entity Manager Factory는 DB당 하나 밖에 사용하지 않는다.

반응형
반응형

MapStruct란?

  • 어노테이션 기반으로 작성되며 Bean으로 등록할 수 있어 여러 프레임워크의 DI를 활용하여 사용할 수도 있다.
  • 타입 세이프하게 객체의 타입 변환 시에 데이터 매핑을 도와주는 어노테이션 프로세서
  • 서버 어플리케이션을 개발할 때 작업하는 DTO 변환 작업은 대부분이 반복적인 작업이 대부분
  • 도메인 객체를 풍부하게 사용하면서, 반환 데이터가 달라지게 될 경우 이를 적절하고 큰 힘을 들이지 않고 매핑할 수 있도록 도와주는 것이 MapStruct
  • 리플렉션이 아닌 직접 메소드를 호출하는 방식으로 동작하여 속도가 빠름
  • 컴파일 시점에 매핑 정보가 타입 세이프한지를 검증함.
  • 빌드 타임에 매핑이 올바르지 않다면 에러 정보를 log에 띄워줌

SetUp

dependencies{
    implementation 'org.mapstruct:mapstruct:1.4.2.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}

Mapper 정의하기

1. 기본적인 매핑 방법

  • Mapper를 정의하는 가장 단순한 방법으로는 자바 인터페이스에 매핑 메소드를 정의하고 org.mapstruct.Mapper 어노테이션을 붙이면 된다.
@Mapper
public interface CarMapper{
    @Mapping(source = "make", target = "manufacturer")
    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto carToCarDto(Car car);

    @Mapping(source = "name", target = "fullName")
    PersonDto personToPersonDto(Person person);
}
  • @Mapper가 붙은 인터페이스는 MapStruct Code Generator가 해당 인터페이스의 구현체를 생성해준다.
  • 구현체 생성 시 source가 되는 클래스와 target이 되는 클래스의 속성명을 비교하고 자동으로 매핑 코드를 작성한다.
  • 매핑될 속성명이 다를 경우 @Mapping 어노테이션을 통해 매핑정보를 맞춰준다.

위와 같은 Mapper를 생성했다면 MapStruct는 아래와 같은 구현체를 자동으로 생성해준다.

// GENERATED CODE
public class CarMapper implements CarMapper{
    @Override
    public CarDto carToCarDto(Car car){
        if( car == null ) {
            return null;
        }

        CarDto carDto = new CarDto();

        if ( car.getFeatures() != null ){
            carDto.setFeatures( new ArrayList<String> car.getFeatures() );
        }

        carDto.setManufacturer(car.getMake());
        carDto.setSeatCount(car.getNumberOfSeats());
        carDto.setDriver(personToPersonDto(car.getDriver()));
        carDto.setPrice(String.valueOf(car.getPrice()));

        if ( car.getCategory() != null ){
            carDto.setCategory( car.getCategory().toString() );
        }
        carDto.setEngine( engineToEngineDto( car.getEngine() ) );

        return carDto;
    }

    @Override
    public PersonDto personDto(Person person){
        //...
    }

    private EngineDto engineToEngineDto(Engine engine){
        if ( engine == null ){
            return null;
        }

        EngineDto engineDto = new EngineDto();

        engineDto.setHorsePower(engine.getHorsePower());
        engineDto.setFuel(engine.getFuel());

        return engineDto;
    }
}

2. 여러 개의 soruce 파라미터로 매핑 메소드 작성

  • MapStruct는 파라미터가 여러 개인 경우에도 기능을 지원한다. 여러 엔티티들을 합칠 때 용이하다.
@Mapper
public interface AddressMapper {
    @Mapping(source = "person.description", target = "description")
    @Mapping(source = "address.houseNo", target = "houserNumber")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
  • 각 송성들은 이름을 비교하여 자동으로 매핑된다.
  • source들의 속성명이 겹치는 경우 @Mapping 어노테이션을 통해 어느 source의 속성을 매핑할 것인지 명시해줘야 한다.

아래와 같이 source로 주어진 파라미터를 직접적으로 target의 속성에 매핑할 수도 있다.

@Mapper
public interface AddressMapper {
    @Mapping(source = "person.description", target = "description")
    @Mapping(source = "hn", target = "houseNumber")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);
}

source에 포함된 bean 속성을 target에 매핑하는법

@Mapper
public interface CustomerMapper {
    @Mapping(target = "name", source = "recored.name")
    @Mapping(target = ".", source = "record")
    @Mapping(target = ".", source = "account")
    Customer customerDtoToCustomer(CustomerDto customerDto);
}
  • "."는 target의 this를 의미한다.
  • CustomerDto.record와 CustomerDto.account를 매핑할 source로 지정한다.
  • record와 account에 매핑할 속성명이 겹치는 경우에는 @Mapping 어노테이션으로 명시하여 지정해준다. 만약 record와 account가 name이라는 이름의 속성을 둘 다 갖고 있다면 첫번째 @Mapping 어노테이션처럼 어느 source의 name을 사용할 건지 명시해준다.

3. Mapper에 커스텀 메소드 작성

  • MapStruct가 자동으로 생성하는 매핑 코드를 불가피하게 쓰지 못하는 경우가 있다. 이런 경우에 아래 두 가지 방법으로 Mapper에 커스텀 메소드를 작성하여 사용할 수 있게 해준다.

1) 인터페이스에 default 메소드로 커스텀 매핑을 추가하는 방법

@Mapper
public interface CarMapper {
    @Mapping(...)
    CarDto carToCarDto(Car car);

    default PersonDto personToPersonDto(Person person){
        //hand-written mapping logic
    }
}

2) Mapper를 추상 클래스로 정의하는 방법

@Mapper
public abstract class CarMapper{
    @Mapping(...)
    public abstract CarDto carToCarDto(Car car);

    public PersonDto personToPersonDto(Person person){
        //hand-written mapping logic
    }
}

4. MapStruct로 target을 인스턴스로 받아 업데이트

  • MapStruct는 source를 인자로 받아 target으로 변환하여 반환하는 기능이 주 기능이지만 객체를 target으로 하여 업데이트하는 기능도 지원한다.
@Mapper
public interface CarMapper{
    void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}
  • carDto와 car를 매핑하여 car의 속성을 carDto의 속성으로 업데이트해준다.
  • 기본적인 매핑 방법은 MapStruct가 제공하는 기존 매핑 방법들과 동일하다.
  • 리턴 타입을 void 대신에 target 파라미터로 변경하는 것도 가능, 이 경우엔 업데이트된 속성들을 가지고 새로 생성된 target 객체가 반환된다.
  • 업데이트 시 Collection 타입의 속성에 대해서는 아래의 같은 Strategy들을 제공한다.
    • CollectionMappingStrategy.ACCESSOR_ONLY: target 객체의 컬렉션 객체가 clear되고 source의 컬렉션으로 업데이트 한다.
    • CollectionMappingStrategy.ADDER_PREFERRED or CollectionMappingStrategy.TARGET_IMMUTABLE: 기존의 target 객체의 컬렉션 객체를 유지한채로 새로운 데이터를 추가하여 update한다.

5. Mapper의 Builder 사용

  • MapStruct 사용시 target이 immutable한 클래스라면 Builder를 사용하여 매퍼가 구현된다.
    ! immutable한 클래스 -> 필드에 직접 접근할 수도 없고 Setter도 정의하지 않아 임의로 필드를 변경할 수 없게 설계된 클래스

target이 되는 Immutable한 Person 클래스

public class Person{
    private final String name;

    protected Person(Person.Builder builder){
        this.name = builder.name;
    }

    public static Person.Builder builder(){
        return new Person.Builder();
    }

    public static class Builder {
        private String name;

        public Builder name(String name){
            this.name = name;
            return this;
        }

        public Person create(){
            return new Person(this);
        }
    }

}

Builder를 사용한 Mapper 구현체 예시

@Mapper
public interface PersonMapper{
    Person map(PersonDto dto);
}

// GENERATED CODE
public class PersonMapperImpl implements PersonMapper{
    public Person map(PersonDto dto){
        if(dto == null){
            return null;
        }

        Person.Builder builder = Person.builder();
        builder.name(dto.getName());

        return builder.create();
    }
}

Mapper의 생성자 사용

  • MapStruct가 Mapper를 구현할 때 Builder가 있는 지부터 체크한다. 만약 Builder가 정의되어 있지 않다면 생성자를 사용하도록 기본적으로 설계되어 있다. 생성자가 여러개 있는 경우에 MapStruct는 아래와 같은 우선순위로 사용할 생성자를 찾는다.
    • @Default이 붙어있는 생성자를 사용
    • public 레벨의 생성자가 하나만 존재하는 경우 해당 생성자를 사용
    • 파라미터가 없는 기본 생성자를 사용

만약 파라미터가 없는 기본 생성자 없이 여러 생성자가 존재할 경우 MapStruct는 어느 생성자를 사용할 지 판단할 수 없게된다. 이 경우에 컴파일 에러를 발생시킴

public class Vehicle {

    protected Vehicle() { }

    // MapStruct will use this constructor, because it is a single public constructor
    public Vehicle(String color) { }
}

public class Car {

    // MapStruct will use this constructor, because it is a parameterless empty constructor
    public Car() { }

    public Car(String make, String color) { }
}

public class Truck {

    public Truck() { }

    // MapStruct will use this constructor, because it is annotated with @Default
    @Default
    public Truck(String make, String color) { }
}

public class Van {

    // There will be a compilation error when using this class because MapStruct cannot pick a constructor

    public Van(String make) { }

    public Van(String make, String color) { }

}

Mapper 사용하기

MapStruct가 생성한 구현체를 사용하기 위해선 두 가지 방식이 존재한다.

  • Mapper Factory 사용
  • Dependency Injection 사용

Mapper Factory 사용

CarMapper mapper = Mappers.getMapper(CarMapper.class);
위와 같이 org.mapstruct.factory.Mappers 클래스를 사용하면 생성된 Mapper 인스턴스를 받아와 사용할 수 있다. MapStruct는 이 경우에 아래와 같은 관례로 작성하여 사용하길 권장
- 인터페이스 매퍼에 인스턴스를 선언

@Mapper
public interface CarMapper{
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    CarDto carToCarDto(Car car);
}
  • 추상 클래스 매퍼에 인스턴스를 선언
    @Mapper
    public abstract class CarMapper{
      public static final CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
      CarDto carToCarDto(Car car);
    }
    이 패턴을 적용하면 mapper를 사용하기 위해 인스턴스를 새로 생성할 필요없이 싱글톤으로 생성된 인스턴스를 사용할 수 있다.
Car car = ...;
CarDto dto = CarMapper.INSTANCE.carToCarDto(car);

Dependency Injection 사용

DI를 지원하는 프레임워크를 사용한다면 MapStruct로 구현된 매퍼들도 DI를 통해 사용할 수 있다.

  • cdi: 매퍼를 application-scoped CDI bean으로 생성하며 @Inject를 통해 사용할 수 있다
  • spring: 매퍼를 스프링 빈으로 생성하며 @Autowired를 통해 사용할 수 있다
  • jsr330: @javax.inject.Named, @Singleton 어노테이션을 매퍼에 붙여 빈을 생성하며 @Inject를 통해 사용할 수 있다

Spring 예제

@Mapper(componentModel = "spring")
public interface CarMapper{
    CarDto carToCarDto(Car car);
}
  • componentModel 속성에 String으로 위의 내용들(cdi, spring, jsr330) 중 하나를 주입하여 각 프레임워크에 맞는 빈을 생성해줌.

위와 같이 매퍼를 구성하면 Spring Context에 빈이 등록되기 때문에 아래와 같이 사용할 수 있다.

  • @Autowired 사용
@Autowired
private CarMapper mapper;
  • 생성자 인젝션으로 매퍼 빈 주입
@RequiredArgsConstructor
public class CarService{
    private final CarMapper carMapper;
}

 

반응형

+ Recent posts