← 참고자료📚 전체 맵참고자료 →
Spring Framework

REST API 완전 정복

Repository · 전체 흐름 · DTO · MapStruct

Part 1 — API · REST · Controller · Service
Part 2 — Repository · 전체 흐름 · DTO
Part 3 — 예외처리 · Valid · application.yml
Repository — DB와 직접 대화하는 역할

데이터를 꺼내오고, 저장하고, 수정하고, 삭제하는 역할만 담당. JPA Repository부터 Querydsl까지 빠짐없이.

① Repository가 왜 필요한가
❌ Service에 DB 코드 직접
@Service
public class UserService {
  public User 조회(int id) {
    // DB 코드가 Service에 직접!
    String sql = "SELECT * FROM users WHERE id=?";
    // ... SQL 처리 코드 길게 ...
  }
}
문제점

DB를 MySQL에서 다른 걸로 바꾸면 Service 코드도 전부 수정. 역할이 불명확하고 테스트 어려움.

✅ Repository로 분리
@Service
public class UserService {
  public User 조회(int id) {
    // DB 처리는 Repository에 위임
    return userRepository.findById(id);
  }
}

// DB 처리 전부 여기서
public interface UserRepository
    extends JpaRepository<User, Integer> { }
② @Repository
@Repository
@Repository
public class UserRepository { }

// @Component의 특수 버전 → 스프링 빈으로 등록
// 추가로 하는 일: DB 관련 예외를 스프링 표준 예외로 자동 변환
// (DB마다 예외 클래스가 달라서, 어떤 DB 쓰든 같은 예외로 처리 가능)
//
// Spring Data JPA (JpaRepository) 쓰면 @Repository 직접 안 붙여도 됨
// JpaRepository 상속하면 자동으로 처리됨
③ Entity 애너테이션 — DB 테이블과 자바 클래스 연결
Entity 클래스 전체
@Entity           // 이 클래스가 DB 테이블과 연결된 클래스야
@Table(name = "users")  // 연결할 테이블 이름 지정. 생략하면 클래스명이 테이블명
@Getter @Setter
public class User {

    @Id                  // 이 필드가 PK(기본키)야
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    // PK 자동 생성 전략
    // IDENTITY → DB가 알아서 1,2,3... 증가 (AUTO_INCREMENT)
    // SEQUENCE → 시퀀스 객체 사용 (Oracle)
    // AUTO     → DB에 맞게 자동 선택
    private int id;

    @Column(nullable = false)     // NOT NULL
    @Column(unique = true)         // UNIQUE
    @Column(length = 50)           // VARCHAR(50)
    @Column(name = "user_name")    // 컬럼명 직접 지정
    private String name;

    @Transient   // 이 필드는 DB 컬럼과 매핑 안 함
    private String 임시데이터;
}
④ Spring Data JPA — JpaRepository
JpaRepository 상속
// JpaRepository 상속만 하면 기본 CRUD 메서드 전부 자동 생성!
public interface UserRepository extends JpaRepository<User, Integer> {
//                                                    ↑타입  ↑PK타입 (id가 int니까 Integer)
}
⑤ 기본 제공 메서드들
자동 생성되는 기본 메서드
// 조회
userRepository.findById(1);           // id로 조회 → Optional<User>
userRepository.findAll();             // 전체 조회 → List<User>
userRepository.count();               // 전체 개수 → long
userRepository.existsById(1);         // 존재 여부 → boolean

// 저장 / 수정
userRepository.save(user);            // id 없으면 INSERT, id 있으면 UPDATE
userRepository.saveAll(userList);     // 여러 개 한번에 저장

// 삭제
userRepository.deleteById(1);         // id로 삭제
userRepository.delete(user);          // 객체로 삭제
userRepository.deleteAll();           // 전체 삭제
⑥ 메서드 이름으로 쿼리 자동 생성

Spring Data JPA의 가장 강력한 기능. 메서드 이름만 잘 지으면 SQL을 자동으로 만들어준다.

메서드 이름 → SQL 자동 생성
public interface UserRepository extends JpaRepository<User, Integer> {

    // SELECT * FROM users WHERE name = ?
    User findByName(String name);

    // SELECT * FROM users WHERE name = ? AND age = ?
    User findByNameAndAge(String name, int age);

    // SELECT * FROM users WHERE name = ? OR email = ?
    List<User> findByNameOrEmail(String name, String email);

    // SELECT * FROM users WHERE age >= ?
    List<User> findByAgeGreaterThanEqual(int age);

    // SELECT * FROM users WHERE name LIKE '%?%'
    List<User> findByNameContaining(String keyword);

    // SELECT * FROM users WHERE name LIKE '?%'
    List<User> findByNameStartingWith(String prefix);

    // SELECT COUNT(*) > 0 FROM users WHERE email = ?
    boolean existsByEmail(String email);

    // SELECT COUNT(*) FROM users WHERE age >= ?
    long countByAgeGreaterThanEqual(int age);

    // DELETE FROM users WHERE name = ?
    void deleteByName(String name);

    // SELECT * FROM users ORDER BY name ASC
    List<User> findAllByOrderByNameAsc();

    // SELECT * FROM users WHERE age >= ? LIMIT 3
    List<User> findTop3ByAgeGreaterThanEqual(int age);
}
키워드SQL
And, OrAND, OR
GreaterThan / GreaterThanEqual> / >=
LessThan / LessThanEqual< / <=
BetweenBETWEEN
ContainingLIKE '%?%'
StartingWith / EndingWithLIKE '?%' / LIKE '%?'
IsNull / IsNotNullIS NULL / IS NOT NULL
OrderBy + 필드명 + Asc/DescORDER BY
Top/First + 숫자LIMIT
⑦ @Query — 직접 쿼리 작성
@Query + @Modifying
public interface UserRepository extends JpaRepository<User, Integer> {

    // JPQL — 자바 객체 기준으로 쿼리 작성
    @Query("SELECT u FROM User u WHERE u.age >= :age AND u.name LIKE %:keyword%")
    List<User> 복잡한조회(@Param("age") int age,
                         @Param("keyword") String keyword);

    // 수정 쿼리는 @Modifying 필요
    @Modifying
    @Query("UPDATE User u SET u.name = :name WHERE u.id = :id")
    int 이름수정(@Param("id") int id, @Param("name") String name);
}
⑧ JPQL vs 네이티브 쿼리
JPQL (기본, 권장)
테이블명 대신 자바 클래스명으로 쿼리 작성
DB 종류에 상관없이 동작
SELECT u FROM User u ← User는 클래스명
네이티브 쿼리 (진짜 SQL)
DB 특화 기능 써야 할 때
DB 바꾸면 수정 필요
@Query(value = "SELECT * FROM users WHERE age >= :age",
       nativeQuery = true)
List<User> 네이티브조회(@Param("age") int age);
⑨ Optional — null 대신 쓰는 안전한 포장지
Optional이 왜 생겼나?

없는 사용자 조회 → user = null → user.getName() 호출 → NullPointerException!
이게 자바에서 제일 많이 나는 에러. Optional은 이걸 방지하려고 만들어진 것.

Optional 사용법
Optional<User> optional = userRepository.findById(1);

// ① 값이 있는지 확인
optional.isPresent();   // true / false

// ② 없으면 예외 던지기 (실무에서 제일 많이 씀)
User user = optional.orElseThrow(
    () -> new UserNotFoundException("없는 사용자")
);

// ③ 없으면 기본값
User user = optional.orElse(new User());

// ④ 없으면 null (비권장)
User user = optional.orElse(null);

// ⑤ 값이 있을 때만 실행
optional.ifPresent(u -> System.out.println(u.getName()));

// 실무에서 제일 많이 쓰는 패턴:
userRepository.findById(id)
    .orElseThrow(() -> new UserNotFoundException("없는 사용자"));
// → 있으면 꺼내주고, 없으면 예외 던져서 404 응답으로 처리
⑩ Pageable — 페이징 처리
Pageable 사용법
// Repository
public interface UserRepository extends JpaRepository<User, Integer> {
    Page<User> findAll(Pageable pageable);
    Page<User> findByAgeGreaterThanEqual(int age, Pageable pageable);
}

// Service에서 사용
@Transactional(readOnly = true)
public Page<User> 목록(int 페이지번호) {
    Pageable pageable = PageRequest.of(
        페이지번호,    // 0부터 시작 (0 = 첫 번째 페이지)
        10,          // 한 페이지에 몇 개
        Sort.by("name").ascending()  // 정렬
    );
    return userRepository.findAll(pageable);
}

// Page 객체에서 꺼낼 수 있는 것들
page.getContent();        // 실제 데이터 List
page.getTotalElements();  // 전체 데이터 수
page.getTotalPages();     // 전체 페이지 수
page.getNumber();         // 현재 페이지 번호
page.isFirst();           // 첫 페이지인지
page.isLast();            // 마지막 페이지인지
page.hasNext();           // 다음 페이지 있는지
⑪ 연관관계 매핑
연관관계 종류
// User 클래스 (1쪽) — 유저 한 명이 주문 여러 개
@Entity
public class User {
    @OneToMany(mappedBy = "user")  // 1:N — mappedBy 있는 쪽 = 연관관계 주인 아님
    private List<Order> orders = new ArrayList<>();
}

// Order 클래스 (N쪽) — 주문 여러 개가 유저 한 명
@Entity
public class Order {
    @ManyToOne                      // N:1 — mappedBy 없는 쪽 = 연관관계 주인
    @JoinColumn(name = "user_id")  // FK 컬럼명 지정
    private User user;
}

// 연관관계 종류
// @OneToMany  → 1:N (일대다)
// @ManyToOne  → N:1 (다대일)
// @OneToOne   → 1:1 (일대일)
// @ManyToMany → N:N (다대다) — 중간 테이블 Entity 따로 만드는 게 권장
연관관계 주인

mappedBy 있는 쪽 → 연관관계 주인 아님. 읽기만 가능, FK 수정 불가.
mappedBy 없는 쪽 → 연관관계 주인. FK를 관리함.
FK가 있는 테이블 쪽 = 연관관계 주인 → Order 테이블에 user_id(FK)가 있으니까 Order가 주인.

⑫ 즉시로딩 vs 지연로딩
EAGER (즉시로딩)
User 조회하면 orders도 즉시 같이 가져옴
SELECT users + SELECT orders 자동으로 같이 날림
필요 없는 데이터도 다 가져옴 → 성능 저하
LAZY (지연로딩, 권장)
User 조회할 때 orders는 안 가져옴
orders를 실제로 쓸 때 그때 가져옴
성능상 권장
로딩 전략 설정
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)   // 지연로딩 (기본값)
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)  // 즉시로딩

// 기본값:
// @OneToMany → LAZY  (지연로딩)
// @ManyToOne → EAGER (즉시로딩) ← 이걸 LAZY로 바꾸는 게 권장
⚠️ LazyInitializationException — 실무에서 제일 많이 만나는 JPA 에러

지연로딩을 쓸 때 트랜잭션 밖에서 orders에 접근하면 이 에러가 터짐.
영속성 컨텍스트(트랜잭션)가 끝난 뒤에 연관 데이터를 꺼내려 해서 발생.
반드시 @Transactional 안에서만 연관 데이터에 접근해야 함.

⑬ cascade — 연관 데이터 자동 처리
cascade 옵션
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();

// cascade = CascadeType.ALL
// → User 저장하면 orders도 자동 저장
// → User 삭제하면 orders도 자동 삭제

// CascadeType 종류:
// ALL     → 전부 적용
// PERSIST → 저장할 때만
// REMOVE  → 삭제할 때만
// MERGE   → 수정할 때만
⑭ Querydsl — 동적 쿼리
동적 쿼리가 뭔가?

검색 조건이 있을 수도 없을 수도 있는 경우. 예: 이름으로 검색할 수도, 나이로 검색할 수도, 둘 다 할 수도, 아무것도 안 할 수도 있을 때.
@Query로는 이런 경우 처리가 매우 어려움 → Querydsl이 깔끔하게 해결.

Querydsl 동적 쿼리
public List<User> 동적검색(String name, Integer age) {

    QUser user = QUser.user;  // Querydsl이 자동 생성하는 클래스
    BooleanBuilder builder = new BooleanBuilder();

    // 조건이 있을 때만 추가
    if (name != null) builder.and(user.name.contains(name));
    if (age != null)  builder.and(user.age.goe(age));  // goe = GreaterOrEqual

    return queryFactory
        .selectFrom(user)
        .where(builder)
        .fetch();
}

// name만 있으면  → WHERE name LIKE '%?%'
// age만 있으면   → WHERE age >= ?
// 둘 다 있으면   → WHERE name LIKE '%?%' AND age >= ?
// 둘 다 없으면   → 전체 조회
// Querydsl은 JPA 기본을 완전히 익힌 다음에 배우는 게 순서상 맞음
⑮ Repository 실전 전체 코드
UserRepository.java — 실전 전체
public interface UserRepository extends JpaRepository<User, Integer> {

    // 이메일로 조회
    Optional<User> findByEmail(String email);

    // 이메일 존재 여부
    boolean existsByEmail(String email);

    // 나이 이상 + 페이징
    Page<User> findByAgeGreaterThanEqual(int age, Pageable pageable);

    // 복잡한 조회는 @Query
    @Query("SELECT u FROM User u WHERE u.age BETWEEN :min AND :max")
    List<User> 나이범위조회(@Param("min") int min, @Param("max") int max);
}
Controller → Service → Repository 전체 흐름

요청이 들어와서 응답이 나가기까지 전체 흐름을 하나의 코드로 연결해서 본다.

① 전체 흐름 다이어그램
클라이언트 (브라우저 / 앱)
↓ HTTP 요청 (POST /users)
DispatcherServlet → HandlerMapping → Controller
↓ @RequestBody로 JSON → 자바 객체 변환 후 Service 호출
@Transactional — Service — 비즈니스 로직 처리
↓ Repository 호출
JpaRepository — Repository — DB 처리
↓ DB (MySQL / H2 etc.)
Database
↑ 결과 반환 → Service → Controller
↑ Jackson이 자바 객체 → JSON 변환
클라이언트 HTTP 응답 (201 + JSON)
② POST /users 요청이 들어왔을 때 — 단계별
1. DispatcherServlet이 요청 받음
2. "/users" + POST → UserController.회원가입() 으로 연결
3. @RequestBody → JSON을 UserCreateRequest 객체로 변환
4. userService.회원가입(request) 호출
5. 이메일 중복 체크 (existsByEmail)
6. 비밀번호 암호화
7. userRepository.save(user) → DB에 INSERT
8. 저장된 User → UserResponse DTO로 변환해서 반환
9. ResponseEntity.status(201).body(response)
10. Jackson이 UserResponse 객체 → JSON 변환
11. HTTP 201 응답 + JSON 클라이언트에 전달
③ 회원가입 전체 연결 코드
Controller + Service + Repository 연결
// Entity
@Entity @Table(name="users") @Getter @Setter
public class User {
    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int id;
    @Column(nullable=false) private String name;
    @Column(unique=true, nullable=false) private String email;
    @Column(nullable=false) private String password;
}

// Repository
public interface UserRepository extends JpaRepository<User, Integer> {
    boolean existsByEmail(String email);
    Optional<User> findByEmail(String email);
}

// Service
@Service @RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;

    @Transactional
    public User 회원가입(User user) {
        if (userRepository.existsByEmail(user.getEmail()))
            throw new DuplicateEmailException("이미 있는 이메일");
        return userRepository.save(user);
    }
}

// Controller
@RestController @RequestMapping("/users") @RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @PostMapping
    public ResponseEntity<User> 회원가입(@RequestBody User user) {
        User saved = userService.회원가입(user);
        return ResponseEntity.status(201).body(saved);
    }
}
④ 조회 / 수정 / 삭제 흐름
조회 / 수정 / 삭제
// 조회 (Service)
@Transactional(readOnly = true)
public User 조회(int id) {
    return userRepository.findById(id)
        .orElseThrow(() -> new UserNotFoundException("없는 사용자"));
}
// 조회 (Controller)
@GetMapping("/{id}")
public ResponseEntity<User> 조회(@PathVariable int id) {
    return ResponseEntity.ok(userService.조회(id));
}

// 수정 (Service) — 변경 감지로 자동 UPDATE, save() 필요 없음!
@Transactional
public User 수정(int id, User 수정데이터) {
    User user = userRepository.findById(id)
        .orElseThrow(() -> new UserNotFoundException("없는 사용자"));
    user.setName(수정데이터.getName());
    return user; // 트랜잭션 끝날 때 자동 UPDATE
}
// 수정 (Controller)
@PutMapping("/{id}")
public ResponseEntity<User> 수정(@PathVariable int id, @RequestBody User data) {
    return ResponseEntity.ok(userService.수정(id, data));
}

// 삭제 (Service)
@Transactional
public void 삭제(int id) {
    if (!userRepository.existsById(id))
        throw new UserNotFoundException("없는 사용자");
    userRepository.deleteById(id);
}
// 삭제 (Controller)
@DeleteMapping("/{id}")
public ResponseEntity<Void> 삭제(@PathVariable int id) {
    userService.삭제(id);
    return ResponseEntity.noContent().build();
}
DTO — Entity를 직접 반환하면 안 되는 이유

지금까지 Entity(User)를 직접 주고받았는데, 실무에서는 이렇게 하면 안 된다. DTO가 왜 필요한지부터 MapStruct까지.

① Entity를 직접 반환하면 안 되는 이유
이유 ① 보안
Entity에 password 필드가 있는데 그대로 반환하면 비밀번호가 JSON에 노출됨
이유 ② 불필요한 데이터
클라이언트한테 필요 없는 내부 데이터까지 다 노출됨
이유 ③ 순환참조
User → Order → User → Order... 무한루프로 서버가 터짐
이유 ④ 유연성
클라이언트가 원하는 형태가 Entity 구조와 다를 수 있음
② DTO 클래스 — 요청용 / 응답용 분리
Request / Response DTO
// 요청용 DTO — 클라이언트가 보내는 데이터
@Getter
public class UserCreateRequest {
    private String name;
    private String email;
    private String password;
}

// 응답용 DTO — password 제외! 클라이언트에 돌려주는 데이터
@Getter @AllArgsConstructor
public class UserResponse {
    private int id;
    private String name;
    private String email;
    // password는 여기 없음!
}
③ DTO 적용한 전체 코드
DTO 적용 — Controller + Service
// Controller — DTO로 요청 받고, DTO로 응답
@RestController @RequestMapping("/users") @RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @PostMapping
    public ResponseEntity<UserResponse> 회원가입(
            @RequestBody UserCreateRequest request) {
        return ResponseEntity.status(201)
            .body(userService.회원가입(request));
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> 조회(@PathVariable int id) {
        return ResponseEntity.ok(userService.조회(id));
    }
}

// Service — DTO ↔ Entity 변환 담당
@Service @RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;

    @Transactional
    public UserResponse 회원가입(UserCreateRequest request) {
        if (userRepository.existsByEmail(request.getEmail()))
            throw new DuplicateEmailException("이미 있는 이메일");

        // DTO → Entity 변환
        User user = new User();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        user.setPassword(request.getPassword());

        User saved = userRepository.save(user);

        // Entity → DTO 변환
        return new UserResponse(saved.getId(),
                                  saved.getName(),
                                  saved.getEmail());
    }

    @Transactional(readOnly = true)
    public UserResponse 조회(int id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("없는 사용자"));
        return new UserResponse(user.getId(),
                                  user.getName(),
                                  user.getEmail());
    }
}
④ MapStruct / ModelMapper — DTO ↔ Entity 변환 자동화
왜 필요한가?

DTO ↔ Entity 변환을 매번 직접 하면 코드가 반복되고 길어짐. 필드가 많아지면 변환 코드만 수십 줄.
실무에서는 변환을 자동으로 해주는 라이브러리를 씀.

MapStruct 사용법
// Mapper 인터페이스 정의
@Mapper(componentModel = "spring")
public interface UserMapper {
    UserResponse toResponse(User user);       // Entity → Response DTO
    User toEntity(UserCreateRequest request);  // Request DTO → Entity
}

// Service에서 사용
@Service @RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final UserMapper userMapper;

    @Transactional
    public UserResponse 회원가입(UserCreateRequest request) {
        User user = userMapper.toEntity(request);   // DTO → Entity 한 줄!
        User saved = userRepository.save(user);
        return userMapper.toResponse(saved);         // Entity → DTO 한 줄!
    }
}
MapStructModelMapper
변환 방식 컴파일 시점에 변환 코드 자동 생성 런타임에 리플렉션으로 변환
속도 빠름 상대적으로 느림
타입 안전성 컴파일 시점에 에러 잡음 런타임에 에러 발생 가능
실무 더 많이 씀 (권장) 설정 간단, 소규모 프로젝트
DTO 핵심 정리

Controller → DTO로 요청 받고, DTO로 응답. Entity 직접 노출 금지.
Service → DTO → Entity 변환 후 처리. Entity → DTO 변환 후 반환.
Repository → Entity로만 DB와 대화.
DTO → 요청용(Request)과 응답용(Response) 분리. Entity와 클라이언트 사이의 완충재.

Repository · 전체 흐름 · DTO · MapStruct 완료
Part 3에서 예외처리 · @Valid · application.yml을 이어서 설명합니다
← 참고자료📚 전체 맵참고자료 →