객체 지향 설계의 기본이 되는 5가지 SOLID 원칙에 대해 알아봅시다.
// 잘못된 예
public class User {
private String name;
public void login() { /* 로그인 기능 */ }
public void saveUser() { /* 데이터베이스 저장 기능 */ }
}
// 올바른 예
public class User { /* 사용자 정보 관리 */ }
public class AuthService {
public void login(User user) { /* 로그인 기능 */ }
}
public class UserRepository {
public void saveUser(User user) { /* 데이터베이스 저장 */ }
}
// 잘못된 예
public class Shape {
public String type;
}
public class AreaCalculator {
public double calculate(Shape shape) {
if (shape.type.equals("circle")) {
return /* 원의 넓이 계산 */;
} else if (shape.type.equals("square")) {
return /* 사각형의 넓이 계산 */;
}
}
}
// 올바른 예
public interface Shape {
double calculateArea();
}
public class Circle implements Shape {
public double calculateArea() { return /* 원의 넓이 계산 */; }
}
public class Square implements Shape {
public double calculateArea() { return /* 사각형의 넓이 계산 */; }
}
public class AreaCalculator {
public double calculate(Shape shape) {
return shape.calculateArea(); // 다형성 활용
}
}
// 잘못된 예
class Car {
public void accelerate() {
System.out.println("자동차가 휘발유로 가속합니다.");
}
}
class ElectricCar extends Car {
@Override
public void accelerate() {
throw new UnsupportedOperationException("전기차는 이 방식으로 가속하지 않습니다.");
}
}
// 올바른 예
interface Acceleratable {
void accelerate();
}
class Car implements Acceleratable {
@Override
public void accelerate() {
System.out.println("내연기관 자동차가 가속합니다.");
}
}
class ElectricCar implements Acceleratable {
@Override
public void accelerate() {
System.out.println("전기차가 배터리로 가속합니다.");
}
}
// 잘못된 예
public interface Animal {
void fly();
void run();
void swim();
}
public class Dog implements Animal {
public void fly() { /* 사용하지 않음 */ }
public void run() { /* 달리기 */ }
public void swim() { /* 수영 */ }
}
// 올바른 예
public interface Runnable {
void run();
}
public interface Swimmable {
void swim();
}
public class Dog implements Runnable, Swimmable {
public void run() { /* 달리기 */ }
public void swim() { /* 수영 */ }
}
// 잘못된 예
class EmailNotifier {
public void sendEmail(String message) {
System.out.println("Email 알림: " + message);
}
}
class NotificationService {
private EmailNotifier emailNotifier;
public NotificationService() {
this.emailNotifier = new EmailNotifier(); // 구체 클래스에 의존
}
public void sendNotification(String message) {
emailNotifier.sendEmail(message);
}
}
// 올바른 예
interface Notifier {
void send(String message);
}
class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("Email 알림: " + message);
}
}
class SMSNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("SMS 알림: " + message);
}
}
class NotificationService {
private Notifier notifier; // 인터페이스에 의존
public NotificationService(Notifier notifier) { // 의존성 주입
this.notifier = notifier;
}
public void sendNotification(String message) {
notifier.send(message);
}
}
객체 지향의 핵심은 다형성이지만, 다형성만으로는 OCP와 DIP를 지킬 수 없습니다. Spring이 이 문제를 해결해줍니다!
Spring Container는 객체(Bean)를 생성하고 관리하는 핵심 요소입니다.
역할:
종류:
Spring Container가 관리하는 객체를 의미합니다.
특징:
등록 방법:
// 생성자 주입 예시 (권장)
@Component
public class MyService {
private final MyRepository repository;
@Autowired // 생성자가 하나인 경우 생략 가능
public MyService(MyRepository repository) {
this.repository = repository;
}
}
// Lombok 사용 시
@Component
@RequiredArgsConstructor // final 필드를 위한 생성자 자동 생성
public class MyService {
private final MyRepository repository;
}
Spring은 기본적으로 Bean을 싱글톤으로 관리합니다.
// 일반적인 싱글톤 패턴 예시
public class SingletonService {
private static final SingletonService instance = new SingletonService();
private SingletonService() { }
public static SingletonService getInstance() {
return instance;
}
}
// Spring의 싱글톤 사용 예시
@Component
public class SpringSingletonService {
// Spring이 알아서 싱글톤으로 관리
}
Spring이 자동으로 Bean을 등록하기 위한 기능입니다.
@SpringBootApplication // @ComponentScan 포함
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
같은 타입의 Bean이 여러 개 존재할 때 해결 방법:
@Primary: 여러 Bean 중 우선 순위 부여
@Component
@Primary
public class MainRepository implements Repository { }
@Qualifier: Bean에 별도 구분자 부여
@Component
@Qualifier("mainRepo")
public class MainRepository implements Repository { }
@Component
public class MyService {
@Autowired
public MyService(@Qualifier("mainRepo") Repository repository) { /* ... */ }
}
필드명으로 매칭: 변수명을 Bean 이름과 일치시킴
@Component
public class MyService {
@Autowired
private Repository mainRepository; // mainRepository라는 이름의 Bean 주입
}
List나 Map으로 모든 Bean 주입 받기
@Component
public class MyService {
private final List<Repository> repositories;
@Autowired
public MyService(List<Repository> repositories) {
this.repositories = repositories; // Repository 타입의 모든 Bean이 주입됨
}
}
Spring에서 제공하는 Validation 오류를 보관하는 객체입니다.
@PostMapping("/member")
public String createMember(
@ModelAttribute MemberDto request,
BindingResult bindingResult,
Model model
) {
// 검증 로직
if (request.getAge() == null || request.getAge() < 0) {
bindingResult.addError(
new FieldError("request", "age", "나이는 0 이상이어야 합니다.")
);
}
// 에러 처리
if (bindingResult.hasErrors()) {
return "error";
}
// 성공 로직
return "success";
}
객체의 필드에 제약 조건을 설정하여 유효성을 검증하는 표준화된 방법입니다.
public class MemberDto {
@NotBlank(message = "이름은 필수입니다")
private String name;
@NotNull
@Min(value = 0, message = "나이는 0세 이상이어야 합니다")
@Max(value = 150, message = "나이는 150세 이하여야 합니다")
private Integer age;
@Email(message = "이메일 형식이 올바르지 않습니다")
private String email;
}
@Controller
public class MemberController {
@PostMapping("/members")
public String createMember(
@Validated @ModelAttribute MemberDto dto,
BindingResult bindingResult
) {
if (bindingResult.hasErrors()) {
return "member/createForm";
}
// 성공 로직
return "redirect:/";
}
}
객체 전체에 대한 검증을 위한 방법입니다.
@PostMapping("/order")
public String order(
@Validated @ModelAttribute OrderDto dto,
BindingResult bindingResult
) {
// 객체 검증 (필드 간 관계 검증)
if (dto.getPrice() * dto.getQuantity() < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000},
"총 주문 금액이 10,000원 이상이어야 합니다.");
}
if (bindingResult.hasErrors()) {
return "order/orderForm";
}
// 성공 로직
return "redirect:/orders";
}
동일한 객체에 대해 상황별로 다른 검증을 적용하는 기능입니다.
// 검증 그룹 인터페이스
public interface SaveCheck { }
public interface UpdateCheck { }
// DTO
public class ProductDto {
@NotNull(groups = UpdateCheck.class) // 수정 시에만 필수
private Long id;
@NotBlank(groups = {SaveCheck.class, UpdateCheck.class}) // 모든 경우 필수
private String name;
@Min(value = 1000, groups = SaveCheck.class) // 저장 시에만 최소값 적용
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
private Integer price;
}
// 컨트롤러
@PostMapping("/products")
public String save(
@Validated(SaveCheck.class) @ModelAttribute ProductDto dto,
BindingResult bindingResult
) {
// ...
}
@PutMapping("/products/{id}")
public String update(
@Validated(UpdateCheck.class) @ModelAttribute ProductDto dto,
BindingResult bindingResult
) {
// ...
}
두 애노테이션 모두 Bean Validation을 적용할 수 있지만, 동작 방식에 차이가 있습니다.
@ModelAttribute:
@RequestBody:
// @ModelAttribute 예시
@PostMapping("/form")
public String processForm(
@Validated @ModelAttribute FormDto dto,
BindingResult bindingResult
) {
// ...
}
// @RequestBody 예시
@PostMapping("/api")
public ResponseEntity<Object> processApi(
@Validated @RequestBody ApiDto dto,
BindingResult bindingResult
) {
// ...
}
Spring은 개발자가 비즈니스 로직에 집중할 수 있도록 객체 생성과 의존관계 관리를 대신해주며, Bean Validation을 통해 편리한 데이터 검증 기능을 제공합니다.