- Template 엔진을 사용하는 controller랑 api 사용하는 controller랑 구조 분리.
공통으로 예외 처리를 할 때, 패키지 단위의 구성으로 공통 처리를 많이 함 -> api랑- template이랑 공통 처리할 요소가 다름.
- template의 경우 예외 발생 시 공통 에러 화면이 나와야함.
- api는 공통에러용 json 스펙이 나가야함.
- Controller + ResponseBody(data자체를 json이나 xml로 보내자) : RestController
@PostMapping("api/v1/members")
public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member){
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberResponse {
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
public class Member {
@Id @GeneratedValue
@Column(name="member_id")
private Long id;
@NotEmpty
private String name;
@Embedded //Address의 Embeddable이나 Member의 Embedded 하나만 있어도 가능.
private Address address;
@OneToMany(mappedBy = "member")//order 테이블에 있는 member에 mapping, 읽기전용, 변경 불가능
private List<Order> orders = new ArrayList<>();
- 위의 코드는 Parameter에 Member Entity를 직접 받음.
- 프레젠테이션 계층을 위한 검증 로직이 entity에 들어가 있음.
- entity에 API 검증을 위한 로직이 있음(@NotEmpty 등등)
- API 마다 @NotEmpty 가 필요할 수도 있고 없을 수도 있음.
- 실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를
위한 모든 요청 요구사항을 담기는 어려움.- Entity 변경되면 API 스펙이 변함
- Entity의 name을 username으로 바꾸면 API의 스펙 자체가 변경되어버림!!!
- Entity의 field 중 어느 값이 parameter로 넘어오는 지 api문서를 봐야 알 수 있음.
- API를 만들 때 Entity를 Parameter로 받거나 외부에 노출하면 안됨!!
API 스펙을 위한 별도의 DTO(Data Transfer Object) 사용!
@PostMapping("api/v2/members")
public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request){
Member member = new Member();
member.setName(request.getName());
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberRequest {
private String name;
}
@Data
static class CreateMemberResponse {
@NotEmpty
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
- Entity가 바껴도 API 스펙은 바뀌지 않음.
- Parameter로 뭐가 넘어오는지 확실히 알 수 있음.
- validation도 API에 맞춰서 설정 가능.
- 유지보수할 때 편함.
- Entity를 외부에 노출하지않고 api 스펙에 맞는 별도의 dto 사용하는 게 API 만들 때 정석
@Transactional
public void update(Long id, String name){
Member member = memberRepository.findOne(id); // 영속상태의 member
member.setName(name); // 영속상태의 member의 이름을 setName으로 변경하고, @Transactional에 의해서 @Transactional aop가 끝나는 시점에 commit이 되고, jpa가 영속성 컨테스트 flush하고 db에 commit
}
@PutMapping("api/v2/members/{id}")
public UpdateMemberResponse updateMemberV2(@PathVariable("id") Long id, @RequestBody @Valid UpdateMemberRequest request){
memberService.update(id,request.getName()); // 커맨드와
Member findMember = memberService.findOne(id);// 쿼리 분리
return new UpdateMemberResponse(findMember.getId(),findMember.getName());
}
@Data
static class UpdateMemberRequest{
private String name;
}
@Data
@AllArgsConstructor // 단순 데이터를 반환하는 경우는 사용해도 괜찮음
static class UpdateMemberResponse {
private Long id;
private String name;
}
Entity를 절대 외부에 직접 반환하지말고 항상 DTO로 바꾸기!!!!
@GetMapping("/api/v1/members")
// List를 collection 타입으로 바로 내면 json 배열 타입으로 나가버림 -> 유연성이 확 떨어짐
public List<Member> membersV1() {
return memberService.findMembers();
}
문제점
1. Entity를 직접 노출하면 entity에 있는 정보들이 모두 외부 노출!
2. List를 collection 타입으로 바로 내면 json 배열 타입으로 나가버림 -> 유연성이 확 떨어짐
위의 문제를 해결하기 위해서 Member Entity에 @JsonIgnore 추가
@JsonIgnore // JSON으로 반환 할때 이 필드는 제외 -> 이런 경우 다른 API를 만들 때 문제가 됨
@OneToMany(mappedBy = "member")//order 테이블에 있는 member에 mapping, 읽기전용, 변경 불가능
private List<Order> orders = new ArrayList<>();
문제점
1. 다른 API를 만들 때 문제가 됨. JsonIgnore를 달아둔 필드가 필요한 api도 있을 것!
2. Entity에 프레젠테이션 계층을 위한 logic이 들어갔음 -> entity로 의존관계가 들어와야하는데, entity에서 의존관계가 나가버림 -> 양방향 의존관계가 걸리면서 애플리케이션 수정이 어려워짐
3. 엔티티가 변경되면 API 스펙이 변함
4. 추가로 컬렉션을 직접 반환하면 항후 API 스펙을 변경하기 어려움 -> 별도의 Result 클래스 생성으로 해결
[
{
"id": 1,
"name": "new-hello",
"address": null
},
{
"id": 2,
"name": "member1",
"address": {
"city": "서울",
"street": "test",
"zipcode": "11111"
}
},
{
"id": 3,
"name": "member2",
"address": {
"city": "부산",
"street": "test2",
"zipcode": "22222"
}
}
]
문제점
List를 collection 타입으로 바로 내면 json 배열 타입으로 나가버림 -> 유연성이 확 떨어짐
API 응답 스펙에 맞추어 별도의 DTO 반환!
@GetMapping("api/v2/members")
public Result membersV2() {
List<Member> findMembers = memberService.findMembers();
List<MemberDto> collect = findMembers.stream()
.map(m -> new MemberDto(m.getName()))
.collect(Collectors.toList());
return new Result(collect);
// return new Result(collect.size(),collect);
}
@Data
@AllArgsConstructor
static class Result<T> {
// 필요한 데이터 확장 용이
// private int count;
private T data;
}
@Data
@AllArgsConstructor
static class MemberDto {
private String name;
}
map을 사용해서 Member를 사용해서 MemberDto로 변환
Result < T> 를 사용해서 반환 데이터에 확장성 추가
{
"data": [
{
"name": "member1"
},
{
"name": "member2"
}
]
}
리스트로 바로 반환 하지 않음