사용자 생성, 조회 API 설계하기

JINNI·2024년 5월 21일
0

[TIL] Java+Spring

목록 보기
9/15
post-thumbnail

다음과 같은 API를 설계한다고 생각해보자.

  • 특정 사용자 정보 조회 API
    [GET] api/member/{memberId}
    • HTTP Header
      Content-Type : application/json
    • HTTP Response Body
      {
        "name": "하지은",
        "nickname": "JINNI",
        "age": 23,
        "sopt": {
            "generation": 33,
            "part": "SERVER"
        }
      }
  • 사용자 생성 API
    [POST] api/member

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 : 모든 필드 값을 파라미터로 받는 생성자를 생성하는 어노테이션

🧐@GeneratedValue의 DB id 생성 전략

TABLE, SEQUENCE, IDENTITY, UUID, AUTO의 5가지가 있다.

  • GenerationType.AUTO : 데이터베이스 사용량에 따라 적절한 생성 전략을 자동으로 선택하는 기본 전략
  • GenerationType.IDENTITY : 자동 증가 열 옵션을 사용하여 데이터베이스 자체에서 기본 키 값을 생성. 고유한 값을 생성하기 위한 데이터베이스의 기본 지원에 의존
  • GenerationType.SEQUENCE : 데이터베이스 시퀀스를 사용하여 기본 키 값을 생성. 사용 중인 데이터베이스에 따라 달라지는 데이터베이스 시퀀스 개체의 사용이 필요
  • GenerationType.TABLE : 별도의 데이터베이스 테이블을 사용하여 기본 키 값을 생성하고, 이 테이블을 관리하고 사용하여 기본 키에 고유한 값을 할당
  • GenerationType.UUID : RFC 4122 Universally Unique Identifier를 생성함으로써 Entity에 대한 primary key를 할당해야 함.


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
깃허브 코드 정리

profile
천재 개발자 되기

0개의 댓글