Back-end/Spring Boot

[Spring Boot] REST API 생성 순서

Bay Im 2025. 1. 23. 18:48

이전에 https://imbay.tistory.com/331, https://imbay.tistory.com/97 에서 BaseEntity와 Repository - Service - Controller 개념은 작성했었지만, 시간이 좀 지나서 다시 순서를 되짚어보려고 한다.
(이전에 면접에서 API 개발시 첨부터 끝까지 말로 코딩을 해보라 하셔서,, 갑자기 머리가 하얘져서 잘 말하지 못한 것 같아 다시 순서 되새겨봅니다,,)
 



1. BaseEntity 클래스 생성

1-1. 각 도메인마다 엔티티 클래스를 생성하기 전에, 중복되는 컬럼은 BaseEntity에 미리 만들어 놓는다.
예로 create나 update된 날짜는 각 테이블마다 존재하기 때문에 BaseEntity에 createDate과 updateDate 컬럼을 만들어준다.
예시)

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
    @Column(updatable = false) 
    @CreatedDate 
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime updatedDate;

    @PrePersist
    protected void onCreate() {
        if (createdDate == null) {
            createdDate = LocalDateTime.now();
        }
        updatedDate = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedDate = LocalDateTime.now();
    }
}
  • @MappedSuperclass
    • BaseEntity 상속받는 엔티티가 아래 필드를 자신의 컬럼으로 포함하도록 설정 (이 클래스 자체는 테이블로 매핑되지 않음)
  • @Column(updatable = false)
    • 한번 필드 저장되면 업데이트 되지 않도록
  • @CreatedDate
    • AuditingEntityListener 활성화되어야 동작 (@EntityListeners(AuditingEntityListener.class))
  • @LastModifiedDate
    • 엔티티 수정시 현재 시간 자동 설정

 
 
 

2. 도메인별 엔티티 클래스 생성

2-1. 각 도메인마다 BaseEntity를 상속받은 엔티티 클래스를 생성해준다.
예시) public class LessonEntity extends BaseEntity {
 
2-2. 클래스 위에 어노테이션을 주입한다.
예시)

@Entity
@Table(name = "lesson")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@AllArgsConstructor
@DynamicInsert
public class LessonEntity extends BaseEntity {
  • @Entity
    • 해당 클래스가 DB 테이블과 매핑되는 엔티티임을 선언
    • JPA를 통해 관리
  • @Table(name = "테이블이름")
    • 해당 엔티티가 매핑될 DB 테이블 이름 지정
    • 테이블 이름을 클래스 이름과 다르게 지정할 때 사용
  • @Getter
    • Lombok 어노테이션으로, 모든 필드에 대한 getter 메소드를 자동 생성
  • @NoArgsConstructor(access = AccessLevel.PROTECTED)
    • Lombok 어노테이션으로 매개변수가 없는 기본 생성자 생성
  • @Builder
    • Lombok 어노테이션으로, 빌더 패턴을 사용하여 객체 생성 가능
  • @AllArgsConstructor
    • Lombok 어노테이션으로, 클래스의 모든 필드를 매개변수로 받는 생성자 자동 생성
  • @DynamicInsert
    • Hibernate 어노테이션으로, INSERT SQL 생성시 값이 할당된 필드만 포함되도록 설정
    • 값이 없는 필드에 대해 기본값을 테이블에서 설정 가능
    • 예로 필드에 @ColumnDefault(”0”)과 @Column(nullable = false) 사용할 때 선언

 
2-3. 각 테이블마다 컬럼을 엔티티의 필드로 작성해준다.
예시)

public class LessonEntity extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long lessonId;

    @ManyToOne
    @JoinColumn(name = "host_id", nullable = false)
    private HostEntity hostEntity;

    @OneToOne
    @JoinColumn(name = "category_id", nullable = false)
    private CategoryEntity categoryEntity;

    @Column(nullable = false, length = 30)
    private String title;

    @Column(nullable = false)
    private String location;

   ...생략...

    @ColumnDefault("0")
    @Column(nullable = false)
    private int applicantSum;

    @ColumnDefault("0")
    @Column(nullable = false)
    private boolean isDeleted;

    // 강좌신청 누적인원 업데이트
    public void updateApplicantSum(int applicantSum) {
        this.applicantSum = applicantSum;
    }
  • @Id
    • 해당 필드가 DB 테이블의 기본키
  • @GeneratedValue(strategy = GenerationType.IDENTITY)
    • MySQL의 AUTO_INCREMENT 같이 기본키 자동 생성
  • @ManyToOne, @OneToOne
    • 테이블 다대일, 일대일 관계시 매핑
  • @JoinColumn(name = "외래키 컬럼 이름", nullable = false)
    • name = 은 헤당 테이블에 생성될 외래키 컬럼 이름 지정
    • nullable = false 은 외래키 컬럼 NULL 허용 안함
  • @Column(nullable = false, length = 30)
    • 해당 필드는 컬럼으로 매핑됨을 지정
  • @ColumnDefault("기본값지정")
    • Hibernate 어노테이션으로, 해당 필드의 기본값 지정
    • 필드의 값이 지정되지 않으면 해당 값으로 적용

 
 
 

3. 도메인별 Repository 인터페이스 생성

3-1. 도메인별로 JpaRepository를 상속한 Repository 인터페이스를 생성한다.
상속할 때 해당 엔티티클래스와 Long을 제네릭 타입으로 지정한다.
예시) public interface LessonRepository extends JpaRepository<LessonEntity, Long> {
 
3-2. 인터페이스에 @Repository 어노테이션을 주입한다.
예시)

@Repository
public interface LessonRepository extends JpaRepository<LessonEntity, Long> {

 
3-3. JPA, JPQL, Native SQL 중 필요에 따라 사용하여 메소드를 작성한다. (https://imbay.tistory.com/312 참고)

  • Optional(Wrapper 클래스)를 사용하면 NullPointerException을 방지할 수 있다.
    • 예시) Optional<LessonDateEntity> findLessonDateEntityByLessondateId(Long lessondateId);
  • JPA 사용자 함수
    • findBy + 엔티티 + 필드
    • find + 엔티티 + By + 엔티티
    • And, Or을 메소드 이름에 추가 가능
    • Desc, Asc 추가 가능
    • First5, Last5 같이 n개로 개수 제한 가능
  • JPQL
    • DB에 직접적인 쿼리를 실행하지 않으며 엔티티 테이블에 매핑하여 사용
    • @Query(value=”SQL 문 작성”) 어노테이션 추가
    • FROM 절 뒤에는 엔티티 클래스 이름을 넣어준다. (테이블명이 아닌 클래스이름!)
    • 예시)
      • @Query(value = "SELECT m FROM MemberEntity m WHERE m.userId = :userid")
      • List<MemberEntity> findByUserId_JPQL_Query(String userid);
  • Native SQL
    • DB에 직접적으로 쿼리를 실행시켜 속도가 빠르다. 여러 조인이나 복잡한 쿼리 작성시 적합
    • @Query(value=”SQL 문 작성”, nativeQuery = true) 어노테이션을 추가
    • 추가로 UPDATE, INSERT, DELETE 문은 @Modifying, @Transactional 어노테이션을 추가해야 한다.
    • 예시)
    •     @Query(value = "SELECT * FROM member WHERE user_id = :userid",
                  nativeQuery = true)
          List<MemberEntity> findByUserId_nativeQuery(String userid);
      
          @Modifying
          @Transactional
          @Query(value = "UPDATE member SET user_id = :userid where id = :id",
                  nativeQuery = true)
          int updateById_nativeQuery(Long id, String userid);

 
 
 

4. 도메인별 Service 인터페이스 생성

나는 구현은 따로 Impl 클래스에서 하고, API 목록을 한눈에 볼 수 있도록 인터페이스 생성을 먼저 해주었다.
예시)

public interface LessonService {
    // 개설 클래스 관리- 개설 클래스 목록 출력
    List<OpenedLessonsResDto> openedLessons(Long userId);

    // 클래스 상세보기
    LessonInfoResDto lessonInfo(Long userId, Long lessonId);

    // 클래스 등록하기
    void createLesson(Long userId, CreateLessonReqDto createLessonReqDto);

    // 클래스 전체 조회 (클래스 탐색)
    List<FullLessonResDto> fullLesson();

 
 
 
 

5. 도메인별 dto 패키지 생성

나는 값 전달은 대부분 DTO 클래스로 전달해주었다.
Map, List 등등 전달할 수 있는 방법은 다양하지만 수정할 때 클래스로 찾는게 더 편해서 이렇게 사용했다.
도메인별로 dto 패키지 생성해주고, 그 안에 request와 response 패키지를 생성했다.
예시)

 
 
 
 

6. 도메인별 exception 클래스 생성

6-1. CustomException을 상속받아 exception 클래스를 생성한다.
메시지나 상태코드가 부여도록 메소드를 작성한다.
예시)

public class LessonNotFoundException extends CustomException {
    static String MESSAGE = "LESSON_NOT_FOUND";

    public LessonNotFoundException() {
        super(MESSAGE);
    }

    @Override
    public HttpStatus getHttpStatus() {
        return HttpStatus.NOT_FOUND;
    }
}

 
 
 
 

7. 도메인별 Service 클래스 생성

7-1. Service 인터페이스를 상속한 ServiceImpl 클래스를 생성한다.
예시) public class LessonServiceImpl implements LessonService {
 
7-2. 클래스에 @Service, @ResuiredArgsConstructor 어노테이션을 주입한다.
 
7-3. 필요한 Repository를 private final로 의존성 필드를 작성한다.
예시) private final LessonDateRepository lessonDateRepository;
 
7-4. Service 인터페이스에서 작성한 메소드를 @Override하여 메소드를 구현한다.
각 메소드마다 @Transactional 어노테이션을 줘야 한다. (트랜잭션 단위로 실행)
메소드 구현은 JPA 함수, Repository에서 만든 메소드, custom한 exception 등을 사용하고,
필요한 dto 클래스 생성, stream() 함수, 빌더 등등 사용해서 메소드를 구현한다.

  • JPA 기본함수 종류
    • findAll()
      • 테이블 전체조회
    • findBy열이름()
      • 테이블 조건맞춰 조회
    • save()
      • 테이블 값 추가, 업데이트
    • delete()
    • count()

 
 
 

8. 도메인별 Controller 클래스 생성

8-1. @RestController, @RequiredArgsConstructor 어노테이션을 주입한 Controller 클래스를 생성한다.
도메인별 기본 URI가 있다면, @RequestMapping(”경로”) 어노테이션을 주입한다.
 
8-2. Service 를 private final로 의존성 필드를 작성한다.
예시) private final LessonService lessonService;
 
8-3. POST/GET/DELETE/PUT에 맞춰 경로를 설정하여 메소드를 작성한다. (https://imbay.tistory.com/317 참고)
메소드에 @POST/GET/DELETE/PUTMapping(”경로”) 어노테이션을 주입한다.
나는 Reponse 클래스를 따로 만들어서 해당 값에 맞춰 return 하였다.
예시)

    // 개설 클래스 관리- 개설 클래스 목록 출력
    @GetMapping("/reservation/my/opened")
    public ResponseEntity<ApiResponse> openedLessons(@AuthenticationPrincipal Long userId) {
        List<OpenedLessonsResDto> openedLessons = lessonService.openedLessons(userId);
        return ResponseEntity.ok(new ApiResponse<>(true, "ok", openedLessons));
    }

    // 개설 클래스 상세
    @GetMapping("/reservation/my/opened/{lessonId}")
    public ResponseEntity<ApiResponse> lessonDetail(@PathVariable Long lessonId) {
        List<LessonDetailResDto> lessonDetails = lessonDateService.lessonDetail(lessonId);
        return ResponseEntity.ok(new ApiResponse<>(true, "ok", lessonDetails));
    }
    
		// 클래스 이미지 업로드
    @PostMapping("/lesson/image-upload")
    public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile file) {
        try {
            String imageUrl = s3UploadService.saveFile(file);
            return ResponseEntity.ok(imageUrl);
        } catch (IOException e) {
            return ResponseEntity.status(500).body("파일 업로드 중 오류가 발생했습니다.");
        }
    }

    // 클래스 등록하기
    @PostMapping("/lesson/create")
    public void createLesson(@AuthenticationPrincipal Long userId, @RequestBody CreateLessonReqDto createLessonReqDto) {
        lessonService.createLesson(userId, createLessonReqDto);
    }
  • @PathVariable
    • URL 경로에서 데이터를 추출하는데 사용
    • 예시) /reservation/{lessonId} ⇒ /reservation/123
  • @RequestParam
    • URL의 쿼리 문자열에 데이터 포함시킬 때 사용
    • 필터링, 검색, 페이징 처리 등 파라미터 전달 시 사용
    • URL 뒤에 ?key=value 형식으로 추가
    • 예시) /reservation?userId=1&status=active
  • @RequestBody
    • DTO 클래스를 사용하여 JSON 형식으로 전송
  • @AuthenticationPrincipal
    • 현재 인증된 사용자 정보 전달
  • @POST/GET/DELETE/PUTMapping(”경로”)
    • HTTP POST/GET/DELETE/PUT 요청 처리

 
 
 


다음은 React-TypeScript로 REST API 연동하는 순서를 작성해보겠습니다.

728x90

'Back-end > Spring Boot' 카테고리의 다른 글

[Spring Boot] AWS S3를 사용한 이미지 업로드 방법  (0) 2024.06.27
[Spring Boot] Repository - Service - Controller  (0) 2024.04.21
[Spring Boot] Scheduler  (0) 2024.04.13
[Spring Boot] TDD  (0) 2024.04.13
[Spring Boot] MyBatis  (0) 2024.04.13