Home 예제 프로젝트 API 개발 - 기본
Post
Cancel

예제 프로젝트 API 개발 - 기본

API 개발

화면을 여전히 템플릿 엔진 등으로 만드는 경우도 많지만, Vue.js, React 등을 활용하는 경우가 늘었다.

서버 개발자 입장에서는 서버에서 렌더링하여 HTML을 통해 나타내는 것이 아닌, 화면에 대한 부분은 앱이나 프론트엔드 개발자가 담당하고 서버 개발자는 API를 통해 통신하는 경우가 늘은 것이다.

JPA를 사용하게 되면 단순히 DB에서 SQL로 데이터를 가져와 전달하는 것 뿐 아니라 엔티티라는 존재가 있기 때문에 보다 복잡하다.

따라서 API를 만들 때 어떤 부분을 고려하여 만들어야 하는지 확인해본다.

회원 등록 API

V1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController  
@RequiredArgsConstructor  
public class MemberApiController {  
  
	private final MemberService memberService;  
	  
	@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;  
		}  
	}  
}
  • 문제점
    • 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
    • 엔티티에 API 검증을 위한 로직이 들어간다 (@NotEmpty 등등)
      • 어떤 화면에서는 @NotEmpty 검증이 필요하지 않은 경우가 있을 수 있다.
    • 실제로는 단순히 회원 엔티티만 해도 다양한 API가 만들어지는데 그 여러 API를 위해 하나의 엔티티에 모든 요구사항을 담을 수 없는 것이다.
    • 엔티티가 변경되면 API 스펙이 변한다.

따라서 API 요청 스펙에 맞추어 별도의 DTO를 파라미터로 받아 해결한다.

V2

1
2
3
4
5
6
7
8
9
10
11
12
13
 @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;  
}
  • CreateMemberRequest를 Member 엔티티 대신 RequestBody와 매핑했다.
  • 엔티티와 프레젠테이션 계층을 위한 로직을 분리할 수 있다.
  • 엔티티와 API 스펙을 명확하게 분리할 수 있다.
    • 따라서 엔티티가 변해도 API 스펙은 변하지 않는다.

회원 수정 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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;  
}

memberService.update() 메서드는 변경감지를 이용해 데이터를 수정하는 방식을 사용한다.

  • 회원 수정 시 위 코드에서는 PUT을 사용했는데, put은 전체 내용을 업데이트 할 때 사용하는 것이 맞고, 부분 업데이트가 목적이라면 PATCH나 POST를 사용하는 것이 REST 스타일에 맞다.

회원 조회 API

V1

1
2
3
4
@GetMapping("/api/v1/members")  
public List<Member> membersV1() {  
	return memberService.findMembers();  
}

위 단순한 방법에는 문제가 있다.

  • 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다. (@JsonIgnore 등)
    • 기본적으로 엔티티의 모든 값이 노출된다.
    • 응답 스펙을 맞추기 위한 로직이 추가된다.
      • @JsonIgnore, 별도의 뷰 로직 등
      • member엔티티와 응답해야하는 스펙의 차이가 있을 수 밖에 없다.
    • 엔티티가 변경되면 API 스펙도 변한다.
    • 컬렉션을 직접 반환하게 되면 향후 API 스펙을 변경하기 어렵다.

따라서 조회 또한 별도의 DTO를 반환하는 방식으로 해야 한다.

V2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@GetMapping("/api/v2/members")  
public Result memberV2() {  
	List<Member> findMembers = memberService.findMembers();  
	List<MemberDto> collect = findMembers.stream()  
			.map(m -> new MemberDto(m.getName()))  
			.collect(Collectors.toList());  
	  
	return new Result(collect);  
}  
  
@Data  
@AllArgsConstructor  
static class MemberDto {  
	private String name;  
}  
  
@Data  
@AllArgsConstructor  
static class Result<T> {  
	private T data;  
}
  • 엔티티를 DTO로 변환해 반환한다.
  • 엔티티가 변해도 API 스펙이 변경되지 않는다.

Result 클래스로 컬렉션을 감싼 이유는 향후 필요한 필드를 편하게 추가할 수 있도록 하기 위함이다.

예를 들면 아래와 같이 추가할 수 있다.

1
2
3
4
5
6
7
8
static class Result<T> {
	private int count;
	private T data;
}

//...

return new Result(collect.size(), collect);

그리고 이렇게 DTO로 반환할 경우 V1에서 결과를 보면 json 객체들이 list 안에 담겨 있었다. 컬렉션을 반환했기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
[
	{
		"id": 1,
		"name": "test1"
		//...
	},
	{
		"id": 2,
		"name": "test2"
		//...
	},
	//...
]

하지만 V2에서의 결과를 보면 하나의 json 오브젝트 내에 배열로 원하는 데이터의 값들만 나열되어 있는 것을 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
{
	"data": [
		{
			"name": "test1"
		},
		{
			"name": "test2"
		}
	]
}
This post is licensed under CC BY 4.0 by the author.