์ด๋ฒ SKALA์์ ์คํ๋ง๋ถํธ ํ๋ก์ ํธ๋ฅผ ํ๋ฉด์,
์๊ฒ ๋ ๊ฐ๋ ์ด๋ ๊ณ์ ํท๊ฐ๋ ธ๋ ๋ฌธ๋ฒ์ ํ ๋ฒ์ ์ ๋ฆฌํ ๋ด์ฉ์ด๋ค~!
0. ๊ฐ๋ฐ ๋ฐ ํ์ผ ์์ฑ ์์
- ๋ฐ์ดํฐ ๋ชจ๋ธ → ๋ฆฌํฌ์งํ ๋ฆฌ → ์๋น์ค → ์ปจํธ๋กค๋ฌ → ํ๋ก ํธ์๋
ํ์ ์ ๊ต์๋๊ป์๋ ์ ์์ ์ธ ์์๋ ์์ ๊ฐ๋ค๊ณ ๋ง์ํ์๋ค.
ํ์ง๋ง ์ํฉ์ ๋ฐ๋ผ, ํ๋ก ํธ์๋๊ฐ ๋ณ๋ ฌ์ ์ผ๋ก ๊ธํ๊ฒ ์งํ๋ผ์ผ ํ ๋๋ ์ปจํธ๋กค๋ฌ๋ฅผ ๋จผ์ ์์ฑํ๊ธฐ๋ ํ๋ค๊ณ ํ์ฌ!
์์ ์์๋ ๊ฐ์ธ์ ์ทจํฅ์ ๋ฌ๋ฆฐ ๋ฌธ์ ์ด๊ธฐ ๋๋ฌธ์ ์์ ๋กญ๊ฒ ๋ณ๊ฒฝ์ด ๊ฐ๋ฅํ๋ค!
์ ์ฒด ๋ค ๋ณด์ด๋ฉด,
- Entity
- Repository
- DTO
- Mapper
- Service
- 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์ ๋์ ๊ตฌ์กฐ
- @EnableJpaRepositories ์ค์ ์ ๊ธฐ๋ฐ์ผ๋ก Spring์ด Repository ์ธํฐํ์ด์ค๋ค์ ์ค์บ
- ๋ด๋ถ์ ์ผ๋ก RepositoryFactoryBean์ด ๋์ํ์ฌ, ์ธํฐํ์ด์ค๋ง ๋ณด๊ณ ๊ตฌํ์ฒด๋ฅผ ์์ฑ
- ๊ทธ ๊ตฌํ์ฒด๋ ํ๋ก์(Proxy) ๊ฐ์ฒด์ด๋ฉฐ, ์ค์ DB์ ์ฐ๊ฒฐ๋๋ ๋์์ ์ด ํ๋ก์๋ฅผ ํตํด ์ํ
- ํ์ ์ฟผ๋ฆฌ ๋ฉ์๋์ ๊ฒฝ์ฐ, ๋ฉ์๋ ์ด๋ฆ์ ๋ถ์ํ์ฌ ์ฟผ๋ฆฌ๋ฅผ ์๋ ์์ฑ
- ์: 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์์๋ ์ ๊ฑฐ
'IT > Java' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [Java] SOLID: ๊ฐ์ฒด ์งํฅ ์ค๊ณ์ 5์์น (0) | 2025.04.03 |
|---|---|
| [Java] ๊ฐ์ฒด ์งํฅ ํ๋ก๊ทธ๋๋ฐ์ ํน์ง (2) | 2025.04.02 |
| [ํธ๋ฌ๋ธ์ํ ] "is"๋ก ์์ํ๋ boolean ํ์ ์ JSON ๋ณํ (0) | 2025.04.01 |