카테고리 없음

Layer Architecture + JPA로 User CRUD API 만들기

mooncommit 2026. 4. 9. 20:06

TO DO LIST

  • 3 Layer Architecture 구조 이해하기 Done
  • Entity, JPA 개념 잡기 Done
  • Repository, Service, Controller 작성하기 Done
  • DTO (Request / Response) 설계하기 Done
  • User CRUD API 완성 (저장, 조회, 수정, 삭제) Done
  • ResponseEntity로 HTTP 상태코드 응답 처리하기 Todo

···

3 Layer Architecture 란?

오늘 실습의 핵심 뼈대. 역할에 따라 계층을 나눠서 코드를 관리하는 구조다. 각 계층이 딱 자기 역할만 하도록 분리하는 게 핵심이다.

Client Controller Service Repository DB
계층역할
Controller요청을 받고 응답을 돌려주는 창구
Service비즈니스 로직 처리 (Request → Entity → Response)
RepositoryDB에 직접 접근하는 유일한 계층

Entity가 뭔지 이해하는 데 시간이 걸렸다

처음엔 그냥 클래스 아닌가 싶었는데, JPA를 쓰면 이 클래스가 DB 테이블이랑 1:1로 매핑된다는 걸 배웠다. DB의 행(row) 하나 = Java 객체 하나.

@Getter
@Entity
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 50, nullable = false)
    private String name;

    @Column(unique = true, nullable = false)
    private String email;
    private String address;
}

@NoArgsConstructor(access = AccessLevel.PROTECTED) — JPA는 내부적으로 기본 생성자가 필요하다. 근데 외부에서 new User()처럼 텅 빈 객체를 만드는 건 막고 싶어서 PROTECTED로 설정한다. JPA는 쓸 수 있되, 개발자가 실수로 쓰는 건 컴파일 타임에 막아주는 안전장치.


SQL 없이 DB를 다루는 방법

JPA는 Java 코드로 DB를 다룰 수 있게 해주는 기술이다. 우리가 Java로 메서드를 호출하면 JPA가 알아서 SQL로 번역해서 DB에 보내준다.

Repository는 JpaRepository를 상속받는 것만으로 기본 CRUD 기능을 다 가져온다. 코드 안에 아무것도 안 써도 된다.

public interface UserRepository extends JpaRepository<User, Long> {
    // 아무것도 안 써도 save, findById, findAll, delete 다 됨
}

JpaRepository<User, Long> — 첫 번째는 다룰 엔티티, 두 번째는 그 엔티티의 PK 타입. User의 id가 Long이라서 Long을 넣었다.


Entity를 그대로 쓰면 안 되는 이유

처음엔 Entity를 그대로 반환하면 되지 않나 싶었는데, 두 가지 문제가 있다는 걸 배웠다. 첫째로 password 같은 민감한 정보까지 노출될 수 있고, 둘째로 DB 구조가 바뀌면 API 응답도 같이 바뀌어버린다.

DTO용도특징
CreateUserRequest클라이언트 → 서버id 없음, final 없음 (Spring이 채워줌)
CreateUserResponse서버 → 클라이언트id 있음, final 있음 (값 변경 불가)

Request에 id가 없는 이유 — id는 DB가 자동 생성하는 값이라 클라이언트가 아직 알 수 없다. 저장되고 나서야 id가 생기기 때문에 Response에만 id가 들어간다.


비즈니스 로직이 뭔지 알게 됐다

Service는 데이터를 저장하는 곳이 아니라, 받아서 처리하고 넘겨주는 곳이다. Request를 Entity로 변환 → Repository에 저장 → 결과를 Response로 변환해서 반환하는 흐름을 담당한다.

@Transactional
public CreateUserResponse save(CreateUserRequest request) {
    User user = new User(
        request.getName(), request.getEmail(), request.getAddress()
    );
    User savedUser = userRepository.save(user);

    return new CreateUserResponse(
        savedUser.getId(), savedUser.getName(),
        savedUser.getEmail(), savedUser.getAddress()
    );
}

@RestController vs @Controller

옛날 방식은 서버에서 HTML까지 직접 만들어서 돌려줬다. 요즘은 프론트엔드가 화면을 담당하고 서버는 JSON 데이터만 주고받기 때문에 @RestController를 쓴다. @Controller에 JSON 자동 변환이 더해진 것!

@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/users")
    public CreateUserResponse createUser(@RequestBody CreateUserRequest request) {
        return userService.save(request);
    }

    @GetMapping("/users/{userId}")
    public GetOneUserResponse getOneUser(@PathVariable Long userId) {
        return userService.getOne(userId);
    }

    @PutMapping("/users/{userId}")
    public UpdateUserResponse update(@PathVariable Long userId,
                                         @RequestBody UpdateUserRequest request) {
        return userService.update(userId, request);
    }

    @DeleteMapping("/users/{userId}")
    public void delete(@PathVariable Long userId) {
        userService.delete(userId);
    }
}

···

🐼 오늘 학습을 마치며

아직도 많이 어렵지만 어노테이션 하나하나가 왜 붙는지 어떤 역할을 하는지 공부하며 실습해보니 약간은 알 것 같다.

JAVA에서 하던 것처럼(속성, 생성자, 기능 / Main에서 객체 생성) 흐름은 비슷했다.


하지만 Spring은 클라이언트에서 Http 요청을 하면 Layer Architecture 패턴으로 흘러가고 의존성 주입,

그리고 서로 데이터를 주고 받을 때 DTO를 사용하고 또 Entity에서 데이터를 받아

그걸 넘기고 저장하고 이런 흐름인 것 같다. 계속 코딩하다보니 약간의 패턴이 보였다.


아직 많은 컴포넌트가 낯설게 느껴져서 많이 해보며 익숙해져야 할 것 같다.


내일은 ResponseEntity로 HTTP 상태코드까지 응답에 담는 걸 해볼 예정이다.