IT/Java

[Java] Spring boot ์ž…๋ฌธ์ž๋ฅผ ์œ„ํ•œ ๊ฐœ๋… ์ •๋ฆฌ (feat. ์‚ฌ์‹ค์€ ๋‚ด๊ฐ€ ๋ชฐ๋ผ์„œ ์ •๋ฆฌํ•œ ๊ฑธ ๊ณ๋“ค์ธ)

zi0_0 2025. 4. 14. 02:27

 

์ด๋ฒˆ SKALA์—์„œ ์Šคํ”„๋ง๋ถ€ํŠธ ํ”„๋กœ์ ํŠธ๋ฅผ ํ•˜๋ฉด์„œ,

์•Œ๊ฒŒ ๋œ ๊ฐœ๋…์ด๋‚˜ ๊ณ„์† ํ—ท๊ฐˆ๋ ธ๋˜ ๋ฌธ๋ฒ•์„ ํ•œ ๋ฒˆ์— ์ •๋ฆฌํ•œ ๋‚ด์šฉ์ด๋‹ค~!


0. ๊ฐœ๋ฐœ ๋ฐ ํŒŒ์ผ ์ž‘์„ฑ ์ˆœ์„œ 

  • ๋ฐ์ดํ„ฐ ๋ชจ๋ธ → ๋ฆฌํฌ์ง€ํ† ๋ฆฌ → ์„œ๋น„์Šค → ์ปจํŠธ๋กค๋Ÿฌ → ํ”„๋ก ํŠธ์—”๋“œ

ํ˜„์—…์˜ ๊ต์ˆ˜๋‹˜๊ป˜์„œ๋Š” ์ •์„์ ์ธ ์ˆœ์„œ๋Š” ์œ„์™€ ๊ฐ™๋‹ค๊ณ  ๋ง์”€ํ•˜์˜€๋‹ค. 

ํ•˜์ง€๋งŒ ์ƒํ™ฉ์— ๋”ฐ๋ผ, ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ๋ณ‘๋ ฌ์ ์œผ๋กœ ๊ธ‰ํ•˜๊ฒŒ ์ง„ํ–‰๋ผ์•ผ ํ•  ๋•Œ๋Š” ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•˜๊ธฐ๋„ ํ•œ๋‹ค๊ณ  ํ•˜์‹ฌ!

์œ„์˜ ์ˆœ์„œ๋Š” ๊ฐœ์ธ์˜ ์ทจํ–ฅ์— ๋‹ฌ๋ฆฐ ๋ฌธ์ œ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ž์œ ๋กญ๊ฒŒ ๋ณ€๊ฒฝ์ด ๊ฐ€๋Šฅํ•˜๋‹ค!

 

์ „์ฒด ๋‹ค ๋ณด์ด๋ฉด,

  1. Entity
  2. Repository
  3. DTO
  4. Mapper
  5. Service
  6. Controller

 

1. ์Šคํ”„๋ง์˜ ํ•ต์‹ฌ ๊ฐœ๋…: Bean, DI, ์˜์กด์„ฑ ์ฃผ์ž…

Bean์ด๋ž€?

  • Bean์€ Spring์ด ์ƒ์„ฑํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๊ฐ์ฒด
  • ์ง์ ‘ new ํ‚ค์›Œ๋“œ๋กœ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ , `@Component`, `@Service`, `@Repository`, `@Controller` ๊ฐ™์€ ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ด๋ฉด Spring์ด ์ž๋™์œผ๋กœ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด ๋“ฑ๋ก
  • ์ด๋ ‡๊ฒŒ ๋“ฑ๋ก๋œ ๊ฐ์ฒด๋Š” ๋‹ค๋ฅธ ํด๋ž˜์Šค์—์„œ `@Autowired` ๋˜๋Š” ์ƒ์„ฑ์ž ์ฃผ์ž…์„ ํ†ตํ•ด ์‰ฝ๊ฒŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ 

์˜์กด์„ฑ ์ฃผ์ž…(DI: Dependency Injection)

  • ๋‚ด๊ฐ€ ํ•„์š”ํ•œ ๊ฐ์ฒด๋ฅผ ๋‚ด๊ฐ€ ์ง์ ‘ ๋งŒ๋“ค์ง€ ์•Š๊ณ , Spring์ด ๋Œ€์‹  ๋งŒ๋“ค์–ด์„œ ๋„ฃ์–ด์ฃผ๋Š” ๊ฒƒ
  • ์˜ˆ์‹œ:
@RequiredArgsConstructor
public class StockService {
    private final StockRepository stockRepository;
    private final StockMapper stockMapper;
}
  • ์—ฌ๊ธฐ์„œ final๋กœ ์„ ์–ธํ•˜๋ฉด, ํ•ด๋‹น ๊ฐ์ฒด๋Š” ๋ฐ˜๋“œ์‹œ ์ƒ์„ฑ์ž์—์„œ ์ดˆ๊ธฐํ™”๋˜์–ด์•ผ ํ•จ์„ ์˜๋ฏธ
  • `@RequiredArgsConstructor`๋Š” final ํ•„๋“œ๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š” ์ƒ์„ฑ์ž๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑ
  • ์ƒ์„ฑ์ž ์ฃผ์ž…์„ ์‚ฌ์šฉํ•˜๋ฉด ์˜์กด์„ฑ์ด ๋ช…ํ™•ํžˆ ๋“œ๋Ÿฌ๋‚˜๊ณ , ํ…Œ์ŠคํŠธ๊ฐ€ ์‰ฌ์šฐ๋ฉฐ, ์ปดํŒŒ์ผ ํƒ€์ž„์— ๋ˆ„๋ฝ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Œ

 

2. Spring Data JPA: Repository ๊ตฌ์กฐ์™€ ๋™์ž‘ ์›๋ฆฌ

Repository๋ž€?

  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์ง์ ‘ ์†Œํ†ตํ•˜๋Š” ๊ณ„์ธต
  • JPA์—์„œ๋Š” JpaRepository<T, ID>๋ฅผ ์ƒ์†๋ฐ›์€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด DB์— ์ ‘๊ทผ
  • ์˜ˆ์‹œ:
public interface StockRepository extends JpaRepository<Stock, UUID> {
    Optional<Stock> findByName(String name);
    boolean existsByName(String name);
}

Spring Data JPA์˜ ๋™์ž‘ ๊ตฌ์กฐ

  1. @EnableJpaRepositories ์„ค์ •์„ ๊ธฐ๋ฐ˜์œผ๋กœ Spring์ด Repository ์ธํ„ฐํŽ˜์ด์Šค๋“ค์„ ์Šค์บ”
  2. ๋‚ด๋ถ€์ ์œผ๋กœ RepositoryFactoryBean์ด ๋™์ž‘ํ•˜์—ฌ, ์ธํ„ฐํŽ˜์ด์Šค๋งŒ ๋ณด๊ณ  ๊ตฌํ˜„์ฒด๋ฅผ ์ƒ์„ฑ
  3. ๊ทธ ๊ตฌํ˜„์ฒด๋Š” ํ”„๋ก์‹œ(Proxy) ๊ฐ์ฒด์ด๋ฉฐ, ์‹ค์ œ DB์™€ ์—ฐ๊ฒฐ๋˜๋Š” ๋™์ž‘์€ ์ด ํ”„๋ก์‹œ๋ฅผ ํ†ตํ•ด ์ˆ˜ํ–‰
  4. ํŒŒ์ƒ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ์˜ ๊ฒฝ์šฐ, ๋ฉ”์„œ๋“œ ์ด๋ฆ„์„ ๋ถ„์„ํ•˜์—ฌ ์ฟผ๋ฆฌ๋ฅผ ์ž๋™ ์ƒ์„ฑ
    • ์˜ˆ: findByName(String name) → SELECT * FROM stock WHERE name = ?

์ง์ ‘ ์ฟผ๋ฆฌ ์ž‘์„ฑ (@Query)

@Query("SELECT p FROM Player p LEFT JOIN FETCH p.playerStockList WHERE p.id = :playerId")
Optional<Player> findByIdWithStocks(@Param("playerId") UUID playerId);
  • ๋ณต์žกํ•˜๊ฑฐ๋‚˜ ์กฐ์ธ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ง์ ‘ JPQL ์ž‘์„ฑ ๊ฐ€๋Šฅ

 

3. Entity vs DTO: ์™œ ๋‚˜๋ˆ ์•ผ ํ•˜๋Š”๊ฐ€?

Entity

  • DB ํ…Œ์ด๋ธ”๊ณผ ๋งคํ•‘๋˜๋Š” ํด๋ž˜์Šค
  • ๋‚ด๋ถ€ ๋กœ์ง ํฌํ•จ, ์—ฐ๊ด€๊ด€๊ณ„(@OneToMany ๋“ฑ)๋„ ํฌํ•จ
  • ์˜ˆ: Player, Stock, PlayerStock

DTO(Data Transfer Object)

  • ์š”์ฒญ/์‘๋‹ต์— ๋งž์ถ˜ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ
  • ๋ณดํ†ต ๋ถˆ๋ณ€(immutable) ๊ฐ์ฒด๋กœ ์„ค๊ณ„
  • ์™ธ๋ถ€์— ๋…ธ์ถœ๋˜๋Š” ๊ตฌ์กฐ์ด๊ธฐ ๋•Œ๋ฌธ์— Entity์™€ ๋ถ„๋ฆฌ
public class StockMapper {
    public Stock toEntity(final CreateStockRequest request) { ... }
    public StockResponse toResponse(final Stock stock) { ... }
}

์™œ final์„ ์“ฐ๋Š”๊ฐ€?

  • ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์—์„œ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋„๋ก ๋ณด์žฅ
  • final์€ ์ฝ๊ธฐ ์ „์šฉ์„ ์˜๋ฏธํ•˜๋ฉฐ, ์‹ค์ˆ˜๋กœ ๊ฐ’์„ ๋ฐ”๊พธ๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€

record DTO ์‚ฌ์šฉ (Java 14+)

public record StockResponse(String name, int price) {}
  • ์žฅ์ : ์ƒ์„ฑ์ž, getter, equals, toString ์ž๋™ ์ƒ์„ฑ
  • ๋ชจ๋“  ํ•„๋“œ๊ฐ€ final์ด๋ฉฐ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€

 

4. ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ: @Transactional

์‚ฌ์šฉ ์ด์œ 

  • ํ•˜๋‚˜์˜ ์ž‘์—… ๋‹จ์œ„๋ฅผ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์–ด ์ฒ˜๋ฆฌํ•จ์œผ๋กœ์จ ์ค‘๊ฐ„์— ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์ „์ฒด ์ž‘์—…์„ ๋กค๋ฐฑํ•  ์ˆ˜ ์žˆ์Œ
  • ์กฐํšŒ๋งŒ ํ•˜๋Š” ๊ฒฝ์šฐ readOnly = true๋กœ ์„ค์ •ํ•˜๋ฉด ์„ฑ๋Šฅ ์ตœ์ ํ™”
@Transactional(readOnly = true)
public List<Stock> getStocks() { ... }

๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ

@Transactional
public Stock createStock(CreateStockRequest request) { ... }

 

5. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ: CustomException ์„ค๊ณ„

@Getter
public class CustomException extends RuntimeException {
    private final HttpStatus status;

    public CustomException(String message, HttpStatus status) {
        super(message);
        this.status = status;
    }
}
  • ์‚ฌ์šฉ ์˜ˆ:
throw new CustomException("ํ•ด๋‹น ์ฃผ์‹์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค", HttpStatus.NOT_FOUND);
  • ํ”„๋ก ํŠธ์—”๋“œ๋กœ ์˜๋ฏธ ์žˆ๋Š” ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ HTTP ์ƒํƒœ์ฝ”๋“œ๋ฅผ ์ „๋‹ฌ ๊ฐ€๋Šฅ

์ƒํ™ฉ ์ƒํƒœ ์ฝ”๋“œ

์ƒํ™ฉ ์ƒํƒœ ์ฝ”๋“œ
๋กœ๊ทธ์ธ ํ•„์š” 401 Unauthorized
๊ถŒํ•œ ์—†์Œ 403 Forbidden
๋ฐ์ดํ„ฐ ์—†์Œ 404 Not Found
์ค‘๋ณต ๋ฐ์ดํ„ฐ 409 Conflict
์š”์ฒญ ์˜ค๋ฅ˜ 400 Bad Request
์ •์ƒ ์ƒ์„ฑ 201 Created

 

6. ์š”์ฒญ ๋งคํ•‘ ์–ด๋…ธํ…Œ์ด์…˜๊ณผ ์œ„์น˜

๋ฐ์ดํ„ฐ ์œ„์น˜ ์–ด๋…ธํ…Œ์ด์…˜ ์„ค๋ช…

๋ฐ์ดํ„ฐ ์œ„์น˜  ์–ด๋…ธํ…Œ์ด์…˜ ์„ค๋ช…
์š”์ฒญ ๋ณธ๋ฌธ(JSON) `@RequestBody` JSON → ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜
URL ๊ฒฝ๋กœ ๋ณ€์ˆ˜ `@PathVariable` /stocks/{id} ์™€ ๊ฐ™์€ ํ˜•ํƒœ์—์„œ ID ์ถ”์ถœ
์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ `@RequestParam` ?page=1 ๊ฐ™์€ ์ฟผ๋ฆฌ์—์„œ ๊ฐ’ ์ถ”์ถœ
ํ—ค๋” ๊ฐ’ `@RequestHeader` Authorization ๋“ฑ์˜ ํ—ค๋” ์ถ”์ถœ
์ฟ ํ‚ค ๊ฐ’ `@CookieValue` ์„ธ์…˜ ID ์ถ”์ถœ ๋“ฑ

 

์˜ˆ์‹œ:

@GetMapping("/stocks")
public ResponseEntity<List<StockResponse>> getStocks(@RequestParam int page) { ... }

 

7. ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ: Pageable๊ณผ Page

Pageable pageable = PageRequest.of(page, 10, Sort.by(Direction.DESC, "id"));
Page<Stock> marketStockList = stockRepository.findAll(pageable);
return marketStockList.stream().map(stockMapper::toResponse).toList();
  • Pageable: page ๋ฒˆํ˜ธ, ํฌ๊ธฐ, ์ •๋ ฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ ๊ฐ์ฒด
  • Page<T>: content(List) ์™ธ์—๋„ ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜, ํ˜„์žฌ ํŽ˜์ด์ง€ ๋“ฑ์˜ ์ •๋ณด๋ฅผ ํฌํ•จ
List<StockResponse> result = new ArrayList<>();
for (Stock stock : marketStockList) {
    result.add(stockMapper.toResponse(stock));
}
  • ์œ„ ์ฝ”๋“œ๋Š” `stream`์„ ์‚ฌ์šฉํ•ด ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋Œ€์ฒด ๊ฐ€๋Šฅ

 

8. ์—ฐ๊ด€๊ด€๊ณ„ ์„ค์ •๊ณผ Cascade, OrphanRemoval

@OneToMany(mappedBy = "player", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PlayerStock> playerStockList;
  • `cascade = ALL`: ๋ถ€๋ชจ ์ €์žฅ/์‚ญ์ œ ์‹œ ์ž์‹๋„ ์ž๋™ ์ฒ˜๋ฆฌ
  • `orphanRemoval = true`: ๋ถ€๋ชจ์—์„œ ์ œ๊ฑฐ๋œ ์ž์‹์€ DB์—์„œ๋„ ์ œ๊ฑฐ
Copy