오늘 다루어볼 내용은 Model mapping을 아주 쉽게 해주는 Mapstruct라는 라이브러리를 다루어볼 것이다. 그 전에 Model mapping에 많이 사용되는 ModelMapper와의 간단한 차이를 이야기해보면, Model Mapper는 Runtime 시점에 reflection으로 모델 매핑을 하게 되는데, 이렇게 런타임시에 리플렉션이 자주 이루어진다면 앱성능에 아주 좋지 않다. 하지만 Mapstruct는 컴파일 타임에 매핑 클래스를 생성해줘 그 구현체를 런타임에 사용하는 것이기 때문에 앱 사이즈는 조금 커질수 있지만 성능상 크게 이슈가 없어서 ModelMapper보다 더 부담없이 사용하기 좋다. 그리고 아주 다양한 기능을 제공하기 때문에 조금더 세밀한 매핑이 가능해진다.

 

바로 간단하게 Mapstruct를 맛보자.

 

build.gradle

build.gradle를 채워넣어보자 !

buildscript {
	ext {
		mapstructVersion = '1.3.1.Final'
	}
}

...

// Mapstruct
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"

...

compileJava {
	options.compilerArgs = [
			'-Amapstruct.suppressGeneratorTimestamp=true',
			'-Amapstruct.suppressGeneratorVersionInfoComment=true'
	]
}

 

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class CarDto {
    private String name;
    private String color;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class Car {
    private String modelName;
    private String modelColor;
}
 
@Mapper
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    Car to(CarDto carDto);
}
cs

 

사실 간단한 것이라, 어노테이션만 보아도 어떠한 기능을 제공하는지 확실하다. 하지만 각 클래스에 getter/setter 및 기본생성자는 꼭 생성해주자.(필드명이 동일할 일은 많이 없겠지만, 각 객체의 필드명이 동일하면 @Mapping 어노테이션은 생략가능하다.) 해당 코드로 테스트코드를 간단하게 작성하자.

 

1
2
3
4
5
6
7
8
9
10
11
public class MapstructTest {
 
    @Test
    public void test() {
        CarDto carDto = CarDto.of("bmw x4""black");
        Car car = CarMapper.INSTANCE.to(carDto);
 
        assertEquals(carDto.getName(), car.getModelName());
        assertEquals(carDto.getColor(), car.getModelColor());
    }
}
cs

 

테스트가 성공하는 것을 볼 수 있다. 아주 간단하면서도 파워풀한 라이브러리인 것 같다. 그러면 실제로 생성된 코드를 한번 볼까?

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class CarMapperImpl implements CarMapper {
 
    @Override
    public Car to(CarDto carDto) {
        if ( carDto == null ) {
            return null;
        }
 
        Car car = new Car();
 
        car.setModelName( carDto.getName() );
        car.setModelColor( carDto.getColor() );
 
        return car;
    }
}
cs

 

우리가 작성하지 않았지만, 코드가 생성되었다. 리플렉션과 같이 비용이 큰 기술이 사용되지 않았다. 간단한 validation 코드와 생성자, getter, setter 코드 뿐이다. 이제 더 많은 Mapstruct 기능들을 살펴보자.

 

하나의 객체로 합치기

여러 객체의 필드값을 하나의 객체로 합치기가 가능하다. 특별히 다른 옵션을 넣는 것은 아니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class UserDto {
    private String name;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class AddressDto {
    private String si;
    private String dong;
}
 
@Mapper
public interface UserInfoMapper {
 
    UserInfoMapper INSTANCE = Mappers.getMapper(UserInfoMapper.class);
 
    @Mapping(source = "user.name", target = "userName")
    @Mapping(source = "address.si", target = "si")
    @Mapping(source = "address.dong", target = "dong")
    UserInfo to(UserDto user, AddressDto address);
}
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class UserInfoMapperImpl implements UserInfoMapper {
 
    @Override
    public UserInfo to(UserDto user, AddressDto address) {
        if ( user == null && address == null ) {
            return null;
        }
 
        UserInfo userInfo = new UserInfo();
 
        if ( user != null ) {
            userInfo.setUserName( user.getName() );
        }
        if ( address != null ) {
            userInfo.setDong( address.getDong() );
            userInfo.setSi( address.getSi() );
        }
 
        return userInfo;
    }
}
cs

 

이미 생성된 객체에 매핑

새로운 인스턴스를 생성하는 것이 아니라, 기존에 이미 생성되어 있는 객체에 매핑이 필요한 경우이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Mapper
public interface UserInfoMapper {
 
    UserInfoMapper INSTANCE = Mappers.getMapper(UserInfoMapper.class);
 
    @Mapping(source = "user.name", target = "userName")
    @Mapping(source = "address.si", target = "si")
    @Mapping(source = "address.dong", target = "dong")
    void write(UserDto user, AddressDto address, @MappingTarget UserInfo userInfo);
}
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class UserInfoMapperImpl implements UserInfoMapper {
 
    @Override
    public void write(UserDto user, AddressDto address, UserInfo userInfo) {
        if ( user == null && address == null ) {
            return;
        }
 
        if ( user != null ) {
            userInfo.setUserName( user.getName() );
        }
        if ( address != null ) {
            userInfo.setDong( address.getDong() );
            userInfo.setSi( address.getSi() );
        }
    }
}
 
cs

 

타입 변환

대부분의 암시적인 자동 형변환이 가능하다. (Integer -> String ...) 다음은 조금 유용한 기능이다.

1
2
3
4
5
6
7
8
9
@Mapper
public interface CarMapper {
 
    @Mapping(source = "price", numberFormat = "$#.00")
    CarDto carToCarDto(Car car);
 
    @IterableMapping(numberFormat = "$#.00")
    List<String> prices(List<Integer> prices);
}
cs

 

만약 클래스에 있는 List<Integer> 필드값을 List<String>의 다른 포맷으로 변경하고 싶다면 @IterableMapping 어노테이션을 이용하자. 위와 비슷하게 날짜 데이터를 문자열로 변환하는 dateFormat이라는 옵션도 존재한다.

 

Source, Target mapping policy

매핑될 필드가 존재하지 않을 때, 엄격한 정책을 가져가기 위한 기능을 제공한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ja@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class Car {
    private String modelName;
    private String modelColor;
    private String modelPrice;
    private String description;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class CarDto {
    private String name;
    private String color;
    private Integer price;
}
 
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 타겟이 되는 오브젝트 필드에 대한 정책을 가져간다. Car 클래스에는 description 필드가 있는데, CarDto 클래스에는 해당 필드가 존재하지 않기때문에 컴파일시 컴파일에러가 발생한다.(ERROR, IGNORE, WARN 정책존재) 만약 특정 필드는 해당 정책을 피하고 싶다면 아래와 같이 어노테이션하나를 달아준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
cs

 

null 정책

Source가 null이거나 혹은 Source의 특정 필드가 null일때 적용가능한 정책이 존재한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 Source 오브젝트가 null일때, 기본생성자로 필드가 비어있는 Target 오브젝트를 반환해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(
            source = "description"
            target = "description"
            ignore = true,
            nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT
    )
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 각 필드에 대해 null 정책을 부여한다. 만약 SET_TO_DEFAULT로 설정하면, List 일때는 빈 ArrayList를 생성해주고, String은 빈문자열, 특정 오브젝트라면 해당 오브젝트의 기본 생성자 등으로 기본값을 생성해준다.

 

특정 필드 매핑 무시

특정 필드는 매핑되지 않길 원한다면 @Mapping 어노테이션에 ignore = true 속성을 넣어준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
 
public class MapstructTest {
 
    @Test
    public void test() {
        CarDto carDto = CarDto.of(
                "bmw x4",
                "black",
                10000,
                "description");
        Car car = CarMapper.INSTANCE.to(carDto);
        System.out.println(car.toString());
    }
}
 
result =>
 
Car(modelName=bmw x4, modelColor=black, modelPrice=$10000.00, description=null)
cs

 

매핑 전처리, 후처리

매핑하기 이전과 매핑 이후 특정 로직을 주입시킬 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL,
        componentModel = "spring"
)
public abstract class CarMapper {
 
    @BeforeMapping
    protected void setColor(CarDto carDto, @MappingTarget Car car) {
        if (carDto.getName().equals("bmw x4")) {
            car.setModelColor("red");
        } else {
            car.setModelColor("black");
        }
 
    }
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(target = "modelColor", ignore = true)
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    public abstract Car to(CarDto carDto);
 
    @AfterMapping
    protected void setDescription(@MappingTarget Car car) {
        car.setDescription(car.getModelName() + " " + car.getModelColor());
    }
}
 
<Generate Code>
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
@Component
public class CarMapperImpl extends CarMapper {
 
    @Override
    public Car to(CarDto carDto) {
        if ( carDto == null ) {
            return null;
        }
 
        Car car = new Car();
 
        setColor( carDto, car );
 
        car.setModelName( carDto.getName() );
        if ( carDto.getPrice() != null ) {
            car.setModelPrice( new DecimalFormat( "$#.00" ).format( carDto.getPrice() ) );
        }
        car.setDescription( carDto.getDescription() );
 
        setDescription( car );
 
        return car;
    }
}
cs

 

전처리와 후처리를 위한 메서드는 private을 사용해서는 안된다. 그 이유는 generate된 코드에 전,후 처리 메서드가 들어가는 것이 아니라 추상 클래스에 있는 메서드를 그대로 사용하기 때문이다.

 

이외에도 지원하는 기능이 너무 많다.. 다 못쓰겠다. 아래 공식 레퍼런스를 살펴보자..

 

 

MapStruct 1.3.1.Final Reference Guide

The mapping of collection types (List, Set etc.) is done in the same way as mapping bean types, i.e. by defining mapping methods with the required source and target types in a mapper interface. MapStruct supports a wide range of iterable types from the Jav

mapstruct.org

 

posted by 여성게
:
Web/JPA 2019. 4. 29. 14:52

 

오늘 포스팅할 내용은 간단히 JPA의 cascade 기능이다. 이전 포스팅 중에 해당 내용에 대해 포스팅한적이 있지만 조금 부족한 것같아서 다시 한번 정리할겸 글을 남긴다.

 

영속성 전이(cascade)란 쉽게 말해 부모 엔티티가 영속화될때, 자식 엔티티도 같이 영속화되고 부모 엔티티가 삭제 될때, 자식 엔티티도 삭제되는 등 부모의 영속성 상태가 전이되는 것을 이야기한다. 영속성전이의 종류로는 ALL, PERSIST, DETACH, REFRESH, MERGE, REMOVE등이 있다. 이름만 봐도 어디까지 영속성이 전이되는지 확 눈에 보일 것이다. 여기서는 별도로 각각을 설명하지는 않는다.

 

오늘의 상황 : A와 B라는 엔티티가 존재하고, 두 엔티티의 관계는 @ManyToMany 관계이다. 이 관계는 중간에 Bridge 테이블을 두어 @OneToMany<->@ManyToOne  @OneToMany<->@ManyToOne 관계로 매핑하였다. 그리고 C라는 엔티티는 A엔티티와 @ManyToOne 관계이다. 이렇게 여러개가 조인이 걸려있는 상황에서 cascade를 이용한 PERSIST 예제를 한번 짜보았다.

 

<AEntity Class>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Entity
@Table(name = "TB_A")
@Getter
@Setter
@ToString
public class AEntity {
    
    @Id
    @Column(name = "A_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    @TableGenerator(
            name="SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="A_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy="a",cascade=CascadeType.ALL)
    private List<CEntity> cList = new ArrayList<>();
    
    
    @OneToMany(mappedBy = "aEntity",fetch=FetchType.EAGER,cascade=CascadeType.ALL)
    private List<BridgeEntity> bridges;
}
 
cs

 

<BEntity class>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Entity
@Table(name = "TB_B")
@Getter
@Setter
@ToString
public class BEntity {
    @Id
    @Column(name = "B_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    @TableGenerator(
            name="SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="B_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "bEntity")
    private List<BridgeEntity> bridges;
    
}
 
cs

 

<CEntity class>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Entity
@Table(name = "TB_C")
@Getter
@Setter
@ToString
public class CEntity {
    
    @Id
    @Column(name = "C_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    @TableGenerator(
            name="SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="C_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name="A_ID", nullable=false)
    private AEntity a;
}
 
cs

 

<BridgeEntity class>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Entity
@Table(name = "TB_BRIDGE")
@Getter
@Setter
@ToString
public class BridgeEntity {
    
    @Id
    @Column(name = "BRIDGE_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    @TableGenerator(
            name="SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="BRIDGE_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "A_ID")
    private AEntity aEntity;
    
    @ManyToOne
    @JoinColumn(name = "B_ID")
    private BEntity bEntity;
}
 
cs

 

<A,B Entity Repository class>

1
2
3
4
5
6
7
public interface AEntityRepository extends JpaRepository<AEntity, Long>{
 
}
 
public interface BEntityRepository extends JpaRepository<BEntity, Long>{
 
}
cs

 

테스트 상황 : A,B,C,BridgeEntity 모두 데이터를 persist 하는 상황이다. 그럼 여기서 생각해야 할것이 있다. 우선 C의 부모는 A이다. 즉, A 엔티티가 persist될때 C 자식 엔티티까지 persist되도록 영속성 전이기능을 이용하면 된다. 그렇다면 A,B,Bridge 세개의 관계는 어떻게 될까? 우선 조건이 있다. A,B,Bridge 관계에서 Bridge엔티티가 외래키의 주인이다. 이 말은 즉슨, Bridge 엔티티가 persist 되기 전에 A,B모두가 영속화된 상태여야하는 것이다. 예제는 아래와 같은 시나리오로 테스트했다.

 

1)B 엔티티 영속화 ->2)A 엔티티의 영속화 + Bridge 엔티티 영속성전이

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringDataJpaTestApplicationTests {
    
    
    @Autowired AEntityRepository aRepository;
    @Autowired BEntityRepository bRepository;
    
    @Test
    public void contextLoads() {
        
        AEntity a = new AEntity();
        a.setName("A");
        
        BEntity b = new BEntity();
        b.setName("B");
        
        IntStream.rangeClosed(09).forEach((i)->{
            CEntity c = new CEntity();
            c.setName(i+"");
            a.getCList().add(c);
        });
        
        a.getCList().stream().forEach((c)->{
            c.setA(a);
        });
        
        
        BridgeEntity bridge = new BridgeEntity();
        bridge.setBEntity(bRepository.save(b));
        bridge.setAEntity(a);
 
        a.setBridges(Arrays.asList(bridge));
        
        aRepository.save(a);
    }
 
}
cs

중요한 것이 있다. 부모 엔티티가 영속화되는 동시에 자식 엔티티가 영속화되게 영속성 전이를 이용하기 위해서는 반드시 자식 객체에 부모객체를 set해주어야 하는 점이다. 만약 부모엔티티를 자식 엔티티에 set해주지 않으면 외래키에 null값이 채워질 것이다.

 

결과

=>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
A_ID NAME
25     A
 
B_ID NAME
21     B
 
C_ID NAME A_ID
63     c-0  25
64     c-1  25
65     c-2  25
66     c-3  25
67     c-4  25
68     c-5  25
69     c-6  25
70     c-7  25
71     c-8  25
72     c-9  25
 
BRIDGE_ID A_ID B_ID
21          25   21
cs
posted by 여성게
:
Web/Spring 2019. 4. 10. 21:29

컨트롤러에서 요청을 엔티티객체로 받는 경우가 있다. 이럴경우 받은 엔티티 객체로 DB까지 로직들이 순차적으로 수행이 될것이다. 그런데 만약 엔티티를 조회하거나, 리스트를 조회하는 경우가 있다고 가정해보자. 그렇다면 요청을 받고 엔티티객체를 조회한 후에 컨트롤러에서 응답값으로 ResponseEntity body에 엔티티객체를 실어 보낼 수 있다. 하지만 여기에서 만약 엔티티객체에서 내가 보내기 싫은 데이터가 포함되어있다면? 그것이 만약 유저정보에 대한 것이고, 그 객체에 패스워드까지 존재한다면? 상상하기 싫은 상황일 것이다. 여기서 해결할 수 있는 방법은 몇가지 있다. 예를 들어 @JsonIgnore,@JsonProperty로 응답을 JSON으로 반환하기 할때 원하는 인스턴스 변수를 제외하고 보낼 수도 있고, 응답용 DTO를 만들어서 응답을 다른 객체로 convert한 후에 보낼 수도 있을 것이다. 어노테이션을 이용한 방법은 추후에 다루어볼것이고, 오늘 다루어 볼 것은 응답용 DTO를 만들어서 응답을 보내는 예제이다. 

 

만약 응답용 DTO를 만들어서 내가 응답으로 내보내고 싶은 정보만 인스턴스변수로 set해서 보내면 될것이다. 하지만 여기서 아주 노가다가 있을 것이다. 그것은 따로 Util 용 Method로 빼서 일일이 set,get하는 방법일 것이다. 만약 한두개의 인스턴스 변수라면 상관없지만 만약 응답으로 내보낼 인스턴수 변수가 아주 많다면 이만한 노가다성 작업도 없을 것이다. 오늘 여기서 소개해줄 해결사가 바로 ModelMapper라는 객체이다. 바로 예제 코드로 들어가겠다.

 

오늘 만들어볼 예제는 이전 포스팅에서 다루어봤던 미완성 GenericController의 응용이다. 자세한 설명은 밑의 링크에서 참고하면 될듯하다.

▶︎▶︎▶︎2019/03/22 - [Spring] - Spring - Springboot GenericController(제네릭컨트롤러), 컨트롤러 추상화

 

1
2
3
4
5
        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>2.3.0</version>
        </dependency>
cs

ModelMapper를 사용하기 위해서 의존성을 추가해준다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class CommonBean {
    
    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}
cs

사용할 ModelMapper클래스를 빈으로 등록해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface DomainMapper {
    
    public <D,E> D convertToDomain(E source,Class<extends D> classLiteral);
}
 
@Component
public class DomainMapperImpl implements DomainMapper{
    
    private ModelMapper modelMapper;
    
    public DomainMapperImpl(ModelMapper modelMapper) {
        this.modelMapper=modelMapper;
    }
    
    /*
     * 공통 매퍼
     */
    @Override
    public <D, E> D convertToDomain(E source, Class<extends D> classLiteral) {
        return modelMapper.map(source, classLiteral);
    }
 
}
cs

그리고 ModelMapper를 이용하여 추후에 많은 유틸메소드(도메인 클래스의 인스턴스를 조작하기 위함)를 만들 가능성이 있을 수도 있기 때문에 따로 유틸로 클래스를 만들어서 해당 클래스내에서 ModelMapper를 활용할 것이다. 여기서는 따로 유틸메소드는 없고 하나의 메소드만 있다. 이것은 엔티티클래스와 매핑할 도메인클래스의 클래스리터럴(Domain.class)를 매개변수로 받아서 ModelMapper.map(엔티티클래스,도메인클래스리터럴) 메소드에 전달한다. 메소드의 반환값으로는 도메인클래스를 반환한다. 여기서 도메인클래스 필드에 엔티티클래스의 필드를 매핑할때는 필드명으로 비교를 한다. 아래에서도 다시 설명하겠지만, 도메인 클래스를 만들때는 엔티티 클래스에서 Http Response로 보내고 싶은 데이터의 필드의 이름과 동일하게 도메인클래스의 필드명으로 만들어줘야하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
 * BaseEntity 추상클래스
 * 해당 추상클래스를 상속할때 @Tablegenerator는 상속하는 클래스에서 정의해야함
 * 또한 id field의 칼럼속성도 필요할때에 재정의해야함
 * @author yun-yeoseong
 *
 */
@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
@Setter
@Getter
@EqualsAndHashCode(of="id")
public abstract class BaseEntity<extends BaseEntity<?>> implements Comparable<T>{
    
    @Id
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    private long id;
    
    @JsonProperty(access=Access.READ_ONLY)
    @Column(name="CREATED_DATE",nullable=false,updatable=false)
    @CreatedDate
    private LocalDateTime createdDate;
    
    @JsonProperty(access=Access.READ_ONLY)
    @Column(name="UPDATED_DATE",nullable=false)
    @LastModifiedDate
    private LocalDateTime modifiedDate;
    
//    @Column(name="CREATED_BY",updatable=false/*,nullable=false*/)
//    @CreatedBy
    
//    @Column(name="UPDATED_BY",nullable=false)
//    @LastModifiedBy
 
    @Override
    public int compareTo(T o) {
        if(this == o) return 0;
        return Long.compare(this.getId(), o.getId());
    }
 
}
 
/**
 * Main Category Entity
 * @author yun-yeoseong
 *
 */
@Entity
@Table(name="MAIN_CATEGORY"
        ,indexes=@Index(columnList="MAIN_CATEGORY_NAME",unique=false))
@AttributeOverride(name = "id",column = @Column(name = "MAIN_CATEGORY_ID"))
@TableGenerator(name="SEQ_GENERATOR",table="TB_SEQUENCE",
                pkColumnName="SEQ_NAME",pkColumnValue="MAIN_CATEGORY_SEQ",allocationSize=1)
@Getter
@Setter
@ToString
public class MainCategoryEntity extends BaseEntity<MainCategoryEntity> implements Serializable{
    
    private static final long serialVersionUID = 5609501385523526749L;
    
    @NotNull
    @Column(name="MAIN_CATEGORY_NAME",nullable=false)
    private String mainCategoryName;
    
}
cs

이번 예제에서 사용할 엔티티 클래스이다. 우선 공통 엔티티 필드들은 BaseEntity라는 추상클래스로 뺐다. 그리고 정의할 엔티티클래스에서 해당 BaseEntity를 상속하여 정의하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@Setter
@ToString
public class MainCategoryDTO implements Serializable{
 
    private static final long serialVersionUID = 3340033210827354955L;
    
    private Long id;
    
    private String mainCategoryName;
    
}
cs

이번 예제에서 사용할 도메인클래스이다. 엔티티의 생성일과 수정일은 굳이 반환할 필요가 없기때문에 도메인클래스 필드에서 제외하였다. 그러면 엔티티 클래스에서 생성일, 수정일을 제외하고 클라이언트에게 값이 반환될 것이다. 여기서 중요한 것은 앞에서도 한번 이야기 했지만, ModelMapper는 매핑에 사용할 필드들을 이름으로 비교하기 때문에 엔티티클래스에서 사용하는 필드이름과 도메인 클래스에서 매핑할 필드이름이 동일해야한다는 것이다. 이점은 꼭 기억해야 할 것 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
 * 
 * GenericController - 컨트롤러의 공통 기능 추상화(CRUD)
 * 재정의가 필요한 기능은 @Override해서 사용할것
 * 
 * @author yun-yeoseong
 *
 * @param <E> Entity Type
 * @param <D> Domain Type
 * @param <ID> Entity key type
 */
@Slf4j
public abstract class GenericController<E,D,ID> {
    
    private JpaRepository<E, ID> repository;
    
    private DomainMapper domainMapper;
    
    private Class<D> dtoClass ;
    
    @SuppressWarnings("unchecked")
    public GenericController(JpaRepository<E, ID> repository,DomainMapper domainMapper) {
        
        this.repository = repository;
        this.domainMapper = domainMapper;
        
        /*
         * 타입추론로직
         */
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[1];
        
        if (type instanceof ParameterizedType) {
            this.dtoClass = (Class<D>) ((ParameterizedType) type).getRawType();
        } else {
            this.dtoClass = (Class<D>) type;
        }
 
    }
    
    /*
     * 엔티티아이디로 조회
     */
    @GetMapping("/{id}")
    public  ResponseEntity<D> select(@PathVariable ID id) {
        log.info("GenericController.select - {}",id);
        E e = repository.findById(id).get();
        D d = domainMapper.convertToDomain(e, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.OK);
    }
    
    /*
     * 리스트 조회
     */
    @GetMapping
    public ResponseEntity<List<D>> list(){
        log.info("GenericController.list");
        List<E> lists = repository.findAll();
        List<D> response = lists.stream().map(e->domainMapper.convertToDomain(e, dtoClass)).collect(Collectors.toList());
        return new ResponseEntity<List<D>>(response, HttpStatus.OK);
    }
    
    /*
     * 엔티티 생성
     */
    @Transactional
    @PostMapping
    public ResponseEntity<D> create(@RequestBody E e) {
        log.info("GenericController.create - {}",e.toString());
        log.info("dtoClass type = {}",dtoClass.getName());
        E created = repository.save(e);
        D d = domainMapper.convertToDomain(created, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.CREATED);
    }
    
    /*
     * 엔티티 수정
     */
    @Transactional
    @PutMapping("/{id}")
    public ResponseEntity<D> update(@RequestBody E t) {
        log.info("GenericController.update - {}",t.toString());
        E updated = repository.save(t);
        D d = domainMapper.convertToDomain(updated, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.CREATED);
    }
    
    /*
     * 엔티티 삭제
     */
    @SuppressWarnings("rawtypes")
    @Transactional
    @DeleteMapping("/{id}")
    public ResponseEntity<?> delete(@PathVariable ID id) {
        log.info("GenericController.delete - {}",id);
        repository.deleteById(id);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}
 
cs

오늘 예제로 살펴볼 GenericController이다. 주석에 나와있는 데로 E,D,ID 제네릭타입은 순서대로 엔티티클래스타입,도메인클래스타입,엔티티클래스 ID타입이다. 그리고 위에서 만든 DomainMapper라는 클래스를 이용하여 사용자 요청에 대한 결과값을 조작하여 도메인클래스로 변경하여 리턴하고 있다.(엔티티에서 반환할 데이터만 DTO로 정의해 반환함) 그리고 하나 더 설명할 것은 이전 포스팅에서 다루었던 제네릭 컨트롤러와는 다르게 제네릭 타입으로 도메인클래스 타입을 받을 수 있게 하나 선언하였고, 해당 도메인 클래스의 타입을 추론하는 로직을 생성자에 한번 넣어주었다. 왜냐하면 도메인 클래스는 직접적으로 실체를 매개변수로 받고 있지 않기 때문에 타입을 추론하여 DomainMapper의 메소드의 매개변수로 들어가야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@RestController
@RequestMapping("/category/main")
public class MainCategoryController extends GenericController<MainCategoryEntity, MainCategoryDTO , Long>{
    
    private MainCategoryService mainCategoryService;
 
    public MainCategoryController(JpaRepository<MainCategoryEntity, Long> repository, DomainMapper domainMapper,
            MainCategoryService mainCategoryService) {
        super(repository, domainMapper);
        this.mainCategoryService = mainCategoryService;
    }
    
}
 
cs

마지막으로 제네릭 컨트롤러를 상속한 컨트롤러이다. 단순 CRUD 메소드가 추상화 되어있기 때문에 굉장히 깔끔한 코드가 되었다. 이 예제 코드를 실행시켜보면 반환 객체로 DTO가 나가는 것을 확인할 수 있다. 

 

여기까지 ModelMapper를 다루어봤다. 굉장히 노가다성 작업을 줄여주는 좋은 놈인것 같다. 글을 읽고 혹시 틀린점이 있다면 꼭 지적해주었음 좋겠다.

posted by 여성게
:
Web/JPA 2018. 10. 14. 19:24

JPA는 쿼리가 너무 정적이라는 단점이 존재합니다. 이러한 단점을 극복하기 위하여 JAP+QueryDSL을 통해 type-safe하고, 자바의 메소드를 통한 동적인(유연한) 쿼리 작성이 가능해집니다.


이전 글에서 JPA사용법에 대하여 간단히 작성했지만 이번 글에서도 JPA부터 설명하겠습니다.



pom.xml 설정


JPA 및 querydsl 등 각종 dependency 설정입니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.spring</groupId>
    <artifactId>jpa</artifactId>
    <name>springjpa</name>
    <packaging>war</packaging>
    <version>1.0.0-BUILD-SNAPSHOT</version>
    <properties>
        <java-version>1.8</java-version>
        <org.springframework-version>5.0.2.RELEASE</org.springframework-version>
        <org.aspectj-version>1.6.10</org.aspectj-version>
        <org.slf4j-version>1.6.6</org.slf4j-version>
<!--querydsl version 명시-->
        <querydsl.version>4.1.4</querydsl.version>
    </properties>
    <!-- 오라클 저장소 -->
    <repositories>
        <repository>
            <id>codelds</id>
            <url>https://code.lds.org/nexus/content/groups/main-repo</url>
        </repository>
    </repositories>
    <dependencies>
<!--querydsl dependency-->
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>${querydsl.version}</version>
            <scope>provided</scope>
        </dependency>
 
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <version>${querydsl.version}</version>
        </dependency>
 
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
            <version>2.9.0</version>
        </dependency>
<!--JPA dependency-->
        <!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-entitymanager -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.1.11.Final</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-jpa -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>2.0.2.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-orm -->
        <!-- <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> 
            <version>${org.springframework-version}</version> </dependency> -->
 
        <!-- oracle -->
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc6</artifactId>
            <version>11.2.0.3</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ucp</artifactId>
            <version>11.2.0.3</version>
            <scope>compile</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
            <version>2.0</version>
        </dependency>
 
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${org.springframework-version}</version>
            <exclusions>
                <!-- Exclude Commons Logging in favor of SLF4j -->
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${org.springframework-version}</version>
        </dependency>
 
        <!-- AspectJ -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>${org.aspectj-version}</version>
        </dependency>
 
        <!-- Logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${org.slf4j-version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>${org.slf4j-version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${org.slf4j-version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.15</version>
            <exclusions>
                <exclusion>
                    <groupId>javax.mail</groupId>
                    <artifactId>mail</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>javax.jms</groupId>
                    <artifactId>jms</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.sun.jdmk</groupId>
                    <artifactId>jmxtools</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.sun.jmx</groupId>
                    <artifactId>jmxri</artifactId>
                </exclusion>
            </exclusions>
            <scope>runtime</scope>
        </dependency>
 
        <!-- @Inject -->
        <dependency>
            <groupId>javax.inject</groupId>
            <artifactId>javax.inject</artifactId>
            <version>1</version>
        </dependency>
 
        <!-- Servlet -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
 
        <!-- Test -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.7</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-eclipse-plugin</artifactId>
                <version>2.9</version>
                <configuration>
                    <additionalProjectnatures>
                        <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
                    </additionalProjectnatures>
                    <additionalBuildcommands>
                        <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
                    </additionalBuildcommands>
                    <downloadSources>true</downloadSources>
                    <downloadJavadocs>true</downloadJavadocs>
                </configuration>
            </plugin>
<!--maven plugin 추가-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <configuration>
                    <!-- 사용할 자바버전 -->
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArgument>-Xlint:all</compilerArgument>
                    <showWarnings>true</showWarnings>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.2.1</version>
                <configuration>
                    <mainClass>org.test.int1.Main</mainClass>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.mysema.maven</groupId>
                <artifactId>apt-maven-plugin</artifactId>
                <version>1.1.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <configuration>
<!--Q클래스들이 생성될 위치입니다.-->
                            <outputDirectory>target/generated-sources/java</outputDirectory>
                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
 

cs




Entity class 작성


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.spring.jpa.model;
 
import java.io.Serializable;
import java.util.Date;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.PrePersist;
import javax.persistence.Table;
 
@Entity
@Table(name="TBL_USER")
public class UserEntity implements Serializable{
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @Column(name="USER_ID")
    private long id;
    
    private String userName;
    private int age;
    private Date createdAt;
    @Column(name="role")
    @Enumerated(EnumType.ORDINAL) //ORDINAL -> int로 할당 STRING -> 문자열로 할당 
    private UserRole role;
    
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name="NATION_ID")
    private NationEntity nationEntity;
    
    @PrePersist
    public void beforeCreate() {
        createdAt=new Date();
    }
    
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Date getCreatedAt() {
        return createdAt;
    }
    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
 
    public UserRole getRole() {
        return role;
    }
 
    public void setRole(UserRole role) {
        this.role = role;
    }
 
    public NationEntity getNationEntity() {
        return nationEntity;
    }
 
    public void setNationEntity(NationEntity nationEntity) {
        this.nationEntity = nationEntity;
    }
    
    
}
 
cs



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.spring.jpa.model;
 
import java.util.Set;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;
 
@Entity
@Table(name="TBL_NATION")
public class NationEntity {
    
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @Column(name="NATION_ID")
    private long id;
    private String nationName;
    private String nationCode;
    
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getNationName() {
        return nationName;
    }
    public void setNationName(String nationName) {
        this.nationName = nationName;
    }
    public String getNationCode() {
        return nationCode;
    }
    public void setNationCode(String nationCode) {
        this.nationCode = nationCode;
    }
    
}
 
cs


maven plugin에서 JpaAnnotationProccesor가 @Entity를 보고 관련 Q클래스를 생성해줍니다.



maven build




run 해주면 target>generate-sources/java에 해당 Q클래스들이 생성됩니다. 만약 생성이 되었지만 Q클래스들을 인식하지 못하였을 경우에는 build path에 해당 폴더를 add folder해줍니다. 



Repository 작성



1
2
3
4
5
6
7
8
9
10
package com.spring.jpa.repository;
 
import java.util.List;
 
import com.spring.jpa.model.UserEntity;
 
public interface JpaUserRepositoryCustom {
    List<UserEntity> findAllLike(String keyword);
}
 
cs


커스텀레포지터리를 작성해줍니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.spring.jpa.repository;
 
import java.util.List;
 
import javax.persistence.EntityManager;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Repository;
 
import com.querydsl.jpa.JPQLQuery;
import com.spring.jpa.model.QUserEntity;
import com.spring.jpa.model.UserEntity;
 
@Repository
public class JpaUserRepositoryImpl extends QuerydslRepositorySupport implements JpaUserRepositoryCustom{
 
    public JpaUserRepositoryImpl() {
        super(UserEntity.class);
        // TODO Auto-generated constructor stub
    }
 
    @Override
    @Autowired
    public void setEntityManager(EntityManager entityManager) {
        super.setEntityManager(entityManager);
    }
 
    @Override
    public List<UserEntity> findAllLike(String keyword) {
        // TODO Auto-generated method stub
        QUserEntity qUserEntity=QUserEntity.userEntity;
        JPQLQuery<UserEntity> query=from(qUserEntity);
        List<UserEntity> userEntityList=query.where(qUserEntity.userName.like(keyword)).fetch();
        return userEntityList;
    }
 
}
 
cs


커스텀레포지터리 구현체를 만들어줍니다. 주의해야할점은 생성자에서 super(domain.class)입니다. 도메인클래스 인자는 해당 구현체가 필요한 것이 아니라, QuerydslRepositorySupport이 필요한 인자입니다. 꼭 넣어주어야합니다 !!!!




그리고 커스텀인터페이스에서 선언한 메소드를 오버라이드하여, querydsl을 이용한 type-safe한 동적인 쿼리를 작성해줍니다. 여기서 querydsl은 일반 Jpa 레포지토리의 기능을 업그레이드 해주는 역할이라고 보시면 됩니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.spring.jpa.repository;
 
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
 
import com.spring.jpa.model.UserEntity;
 
@Repository("jpaUserRepository")
public interface JpaUserRepository extends JpaRepository<UserEntity, Long>,JpaUserRepositoryCustom{
 
    UserEntity findByUserName(String userName);
    
    @Query(value="SELECT * FROM TBL_USER WHERE USERNAME=?1 AND AGE=?2",nativeQuery=true)
    UserEntity findByUserNameAndAge(String userName,int age);
}
 
cs


일반 Jpa 레포지토리에 커스텀인터페이스 상속을 해줍니다. 이렇게 커스텀인터페이스를 상속해주면 커스텀인터페이스 구현체에서 구현한 querydsl 메소드를 일반 jpa레포지토리에서 사용이 가능해집니다. 즉, 일반 jpa레포지터리의 기능을 그대로 이용하면서 업그레이드된 querydsl쿼리를 같이 이용할 수 있게 되는 것입니다.



Controller 작성


대충짰습니다....controller에서 repository를 다이렉트로 주입하고... 따라하시지 마시고 테스트용도이니 만약 사용하실때는 꼭 controller->service->repository 구조로 이용해주세요 !!!!


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.spring.jpa.controller;
 
import java.util.Arrays;
import java.util.List;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.spring.jpa.model.NationEntity;
import com.spring.jpa.model.UserEntity;
import com.spring.jpa.model.UserRole;
import com.spring.jpa.repository.JpaNationRepository;
import com.spring.jpa.repository.JpaUserRepository;
import com.spring.jpa.service.UserService;
 
@Controller
public class UserController {
    
    private final static Logger log=LoggerFactory.getLogger(UserController.class);
    
    @Autowired
    UserService userService;
    @Autowired
    JpaNationRepository jpaNationRepository;
    @Autowired
    JpaUserRepository jpaUserRepository;
    
    @GetMapping("/user")
    public String getUserView() {
        return "user";
    }
    
    @GetMapping("/usersave")
    public String saveUser(ModelMap map,UserEntity userEntity,NationEntity nationEntity) throws JsonProcessingException {
        
        nationEntity.setNationCode("KR");
        nationEntity.setNationName("대한민국");
        jpaNationRepository.save(nationEntity);
        
        userEntity.setUserName("여성게");
        userEntity.setAge(27);
        userEntity.setRole(UserRole.USER);
        userEntity.setNationEntity(nationEntity);
        userService.save(userEntity);
        
        UserEntity saveUser=userService.findByUserName("여성게");
        UserEntity user=userService.findByUserNameAndAge(saveUser.getUserName(), saveUser.getAge());
        log.info("username={},user age={}",user.getUserName(),user.getAge());
        log.info("nationCode={},nationName={}",user.getNationEntity().getNationCode(),user.getNationEntity().getNationName());
        
        XmlMapper xmlMapper=new XmlMapper();
        String xmlString=xmlMapper.writeValueAsString(user);
        
        log.info(xmlString);
        
        List<UserEntity> userList=jpaUserRepository.findAllLike("여성게");
        log.info("userEntity list={}",userList.get(0).getUserName());
        
        map.addAttribute("userEntity", saveUser);
        return "userlist";
    }
}
 
cs



확실히.. 편하긴 하지만 여기까지 오기에 복잡한 설정은 아니지만 귀찮은 설정들이 많았내요... 조금 적응되면 이 기술도 자유롭게 사용가능하겠죠? mybatis를 선호하였던 저였지만, jpa+querydsl을 잠시나마 사용해보니 확실히 기술을 익히고 사용한다면 mybatis보다 훨씬 유연한 데이터베이스 관련 기술 일것 같습니다.



혹시 여기까지 오시다가 cannot change web module version 2.5 to ....어쩌고 맞나?.. 이런 에러가 뜬다면 자기가 사용중인 web module 버전과 maven에 명시되어 있는 버전과 비교를 하시고 꼭 maven jdk 버전이랑도 같이 맞춰주시면 해당 에러는 사라집니다... 설정을 다 다시 마추고 maven update하면 에러는 싹 사라집니다. 

posted by 여성게
: