다음과 같은 API를 설계한다고 생각해보자.
{
"name": "하지은",
"nickname": "JINNI",
"age": 23,
"sopt": {
"generation": 33,
"part": "SERVER"
}
}
Entity : Member
Entity는 DB와 매핑될 수 있는 구체적인 개념. 실제 개별 객체와 개념을 의미한다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 기본 생성자 만들어주는 어노테이션
public class Member {
/*
@Id는 해당 필드가 식별자임을 명시해주는 어노테이션으로,
@Entity가 붙은 클래스에 반드시 선언되어야 함
@GeneratedValue는 DB에 id를 어떻게 생성할지 전략을 나타내는 어노테이션
*/
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String nickname;
private int age;
@Embedded
private SOPT sopt;
// 파라미터가 있는 생성자 정의
@Builder // Builder 패턴을 이용해 객체를 생성
public Member(String name, String nickname, int age, SOPT sopt) {
this.name = name;
this.nickname = nickname;
this.age = age;
this.sopt = sopt;
}
public void updateSOPT(SOPT sopt) {
this.sopt = sopt;
}
}
@Entity
: DB 테이블에 대응하는 클래스임을 알려주는 어노테이션으로 JPA가 관리할 수 있도록 한다. 접근 제어자가 public 혹은 protected인 기본 생성자가 필수이며 name 속성으로 JPA에서 사용할 Entity 이름을 지정할 수 있다.(지정 안 하면 클래스명으로 기본 설정됨)@Getter
: 필드값을 안전하게 외부로 리턴하기 위해 필요한 어노테이션으로, 하나하나 설정해줄 필요 없이 lombok의 @Getter를 이용해 모든 필드의 Getter 메서드를 구현할 수 있다.@Id
: 해당 필드가 식별자임을 명시해주는 어노테이션. @Entity가 붙은 클래스에는 반드시 선언되어야 함@GeneratedValue
: Database에 id를 어떻게 생성할지 생성 전략을 나타내는 어노테이션@Embedded
과 @Embeddable
: Entity 내에 들어갈 수 있는 데이터들을 Member 클래스의 필드로 모두 선언하는 것이 아니라, 공통적으로 관리할 수 있는 필드들은 한 객체의 속성으로 보고 객체로 관리할 수 있도록 하는 것@Embeddable // @Embedded로 들어갈 class에 붙이는 코드
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class SOPT {
private int generation;
@Enumerated(value = STRING) // 지정하지 않을 경우 Enum 값이 아닌 인덱스 값으로 DB에 저장됨
private Part part;
}
@Getter
@AllArgsConstructor
public enum Part {
SERVER("서버"),
WEB("웹"),
ANDROID("안드로이드"),
IOS("iOS"),
PLAN("기획"),
DESIGN("디자인");
private final String name;
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
: 파라미터가 없는 기본 생성자를 만드는데, 그 생성자의 접근제어를 protected 레벨로 하겠다는 의미. 필드, 생성자, 메소드에 적용되며 자식 클래스가 아닌 다른 패키지에 소속된 클래스는 접근할 수 없다. 같은 패키지 또는 자식 클래스에서 사용할 수 있는 멤버를 만든다.@AllArgsConstructor
: 모든 필드 값을 파라미터로 받는 생성자를 생성하는 어노테이션TABLE, SEQUENCE, IDENTITY, UUID, AUTO의 5가지가 있다.
Controller : MemberController
Client에서 보내는 HTTP Request를 받아 Service 계층에 전달, Service 계층에서 처리한 응답을 받아 처리하는 Class
@RestController
@RequestMapping("/api/member")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
// 특정 사용자 정보 단건 조회 v1
@GetMapping(value = "/{memberId}")
public ResponseEntity<MemberGetResponse> getMemberProfileV1(@PathVariable Long memberId) {
return ResponseEntity.ok(memberService.getMemberByIdV2(memberId));
}
// 특정 사용자 정보 단건 조회 v2
@GetMapping(value = "/{memberId}/v2", consumes = "application/json", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<MemberGetResponse> getMemberProfileV2(@PathVariable Long memberId) {
return ResponseEntity.ok(memberService.getMemberByIdV2(memberId));
}
// 생성
@PostMapping
public ResponseEntity<Void> createMember(@RequestBody MemberCreateRequest request) {
URI location = URI.create(memberService.create(request));
return ResponseEntity.created(location).build();
}
@RestController
: @Controller
와 @ResponseBody
의 기능을 하는 어노테이션으로 @ResponseBody는 Java 객체를 JSON 기반의 HTTP body로 변환하는 역할을 한다. API 개발 시 Controller 위에 붙여줘야 함. @RequestMapping
: class 위에 선언하여 내부 메서드 위에 붙은 @(HTTP Method)Mapping 앞에 붙음. name 속성에 공통된 URL을 입력한다.@RequiredArgsConstructor
: 초기화되지 않은 final 필드나 @NonNull 이 붙은 필드에 대해 생성자를 생성해주는 어노테이션이다. @Autowired를 사용하지 않고 의존성 주입을 할 수 있다. 위 코드에서 MemberService를 주입해준 것을 확인할 수 있다.@PathVariable
: 경로 변수를 표시하기 위해 메서드의 매개 변수에 사용된다. URL에서 {} 안에 적힌 변수값을 추출하여 매개변수로 할당한다.DTO : MemberCreateRequest, MemberGetResponse
API 명세서를 토대로 클라에게 응답할/클라이언트에게서 요청받을 JSON 형태의 DTO를 정의한다.
// MemberCreateRequest
@Data
public class MemberCreateRequest { // Member 생성 API
private String name;
private String nickname;
private int age;
private SOPT sopt;
}
@Data
: @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 모두 합친 lombok의 어노테이션// MemberGetResponse
public record MemberGetResponse (
String name,
String nickname,
int age,
SOPT soptInfo
) { // 사용자 정보를 응답하는 API
public static MemberGetResponse of(Member member) {
return new MemberGetResponse(
member.getName(),
member.getNickname(),
member.getAge(),
member.getSopt()
);
}
}
Service, Domain :
Service와 Domain에서 비즈니스 로직을 구현한다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {
private final MemberJpaRepository memberJpaRepository; // 의존성 주입
// 사용자 조회 API
/* BAD
findById.get()으로 데이터를 가져오는 경우 NullPointerException 발생 위험
1. MemberJpaRepository interface에서 findByIdOrThrow method 구현을 통해 활용
2. MemberService 내부에 private method 만드는 방법
*/
public MemberGetResponse getMemberByIdV1(Long id) {
Member member = memberJpaRepository.findById(id).get();
return MemberGetResponse.of(member);
}
// GOOD - NullPointerException 예외 처리
public MemberGetResponse getMemberByIdV2(Long id) {
return MemberGetResponse.of(memberJpaRepository.findById(id).orElseThrow(
() -> new EntityNotFoundException("존재하지 않는 회원입니다.")));
}
// GOOD - MemberService 내부에 private method 만들어 활용
public MemberGetResponse getMemberByIdV3(Long memberId) {
return MemberGetResponse.of(findById(memberId));
}
private Member findById(Long memberId) {
return memberJpaRepository.findById(memberId).orElseThrow(
() -> new EntityNotFoundException("해당하는 회원이 없습니다.")
);
}
// GOOD - MemberJpaRepository interface에서 구현한 findByIdOrThrow method 활용
public MemberGetResponse getMemberByIdV4(Long id) {
return MemberGetResponse.of(memberJpaRepository.findByIdOrThrow(id));
}
public List<MemberGetResponse> getMembers() {
return memberJpaRepository.findAll()
.stream()
.map(MemberGetResponse::of)
.collect(Collectors.toList());
}
// 사용자 생성 API
@Transactional
public String create(MemberCreateRequest request) {
Member member = memberJpaRepository.save(Member.builder()
.name(request.getName())
.nickname(request.getNickname())
.age(request.getAge())
.sopt(request.getSopt())
.build());
return member.getId().toString();
}
@Service
: Service(자바 로직 처리) 클래스 위에 선언하는 어노테이션. Spring Bean으로 등록한다.@Transactional
: 트랜잭션 처리를 위한 어노테이션으로 추후 더 자세히 공부할 예정!Repository : MemberJpaRepository
DB에 접근할 수 있는 Repository layer
public interface MemberJpaRepository extends JpaRepository<Member, Long> {
// JpaRepository<Entity, Entity의 식별자 타입> 형태로 작성한 후 상속해 사용
default Member findByIdOrThrow(Long id) {
return findById(id).orElseThrow(
() -> new IllegalStateException("Member with id " + id + " not found")
);
}
}
테스트 결과
사용자 생성 API
201 Created가 발생한 걸 확인할 수 있다. (클라와 서버 양측을 위해 request가 성공한 건에 대해 response를 만들고 예외 처리를 좀 더 꼼꼼히 하면 좋을 듯하다. response로 아무것도 안 와서 처음에는 작동이 잘 안되는 줄 알았다.)
사용자 조회 API
H2는 In-memory 모드로 실행하는 경우 테이블을 직접 확인할 수 없다고 한다.
따라서 생성 후 제대로 생성되었는지는 GET으로 직접 조회해봐야 가능함.
참고자료
33기 DO SOPT 서버 파트 2차 세미나 자료(배포 불가)
이것이 자바다 : 신용권의 Java 프로그래밍 정복1
[JPA] 엔티티 매핑 - @Entity, @Table
인프런 - 엔티티 작성시, protected 만드는 구분
Enum Class GenerationType
깃허브 코드 정리