SOLID 원칙
1. 단일 책임 원칙
- 클래스는 단 하나의 책임만 가져야 한다
- 올바른 코드 예시)
// 직원 정보만 관리
public class Employee {
private String name;
private double salary;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getSalary() { return salary; }
}
// 급여 계산 담당
public class PayrollCalculator {
public double calculatePay(Employee employee) {
return employee.getSalary() * 1.1;
}
}
// 데이터베이스 관리 담당
public class EmployeeRepository {
public void save(Employee employee) {
System.out.println("Saving " + employee.getName() + " to database");
}
}
// 리포트 생성 담당
public class ReportGenerator {
public void generateReport(Employee employee) {
System.out.println("Generating report for " + employee.getName());
}
}
2. 개방/폐쇄 원칙
- 클래스는 확장에는 열려있고, 수정에는 닫혀있어야 한다
- 올바른 코드 예시)
// 추상화를 통한 확장 가능한 설계
public interface DiscountStrategy {
double calculateDiscount(double amount);
}
public class RegularCustomerDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.05;
}
}
public class VIPCustomerDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.15;
}
}
// 새로운 할인 정책 추가시 기존 코드 수정 없이 확장 가능
public class PremiumCustomerDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.20;
}
}
public class DiscountCalculator {
public double calculateDiscount(DiscountStrategy strategy, double amount) {
return strategy.calculateDiscount(amount);
}
}
3. 리스코프 치환 원칙
- 자식 클래스는 부모 클래스를 대체할 수 있어야 한다
- 올바른 코드 예시)
// 공통 인터페이스 사용
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
public class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
4. 인터페이스 분리 원칙
- 클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다
- 올바른 코드 예시)
// 인터페이스를 작은 단위로 분리
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public interface Payable {
void receiveSalary();
}
// 필요한 인터페이스만 구현
public class Human implements Workable, Eatable, Sleepable, Payable {
@Override
public void work() {
System.out.println("Human working");
}
@Override
public void eat() {
System.out.println("Human eating");
}
@Override
public void sleep() {
System.out.println("Human sleeping");
}
@Override
public void receiveSalary() {
System.out.println("Human receiving salary");
}
}
public class Robot implements Workable {
@Override
public void work() {
System.out.println("Robot working");
}
// 불필요한 메서드 구현 없음!
}
5. 의존 역전 원칙
- 고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다
- 올바른 코드 예시)
// 추상화 (인터페이스)
public interface MessageSender {
void sendMessage(String message);
}
// 저수준 클래스들이 인터페이스 구현
public class EmailSender implements MessageSender {
@Override
public void sendMessage(String message) {
System.out.println("Sending email: " + message);
}
}
public class SMSSender implements MessageSender {
@Override
public void sendMessage(String message) {
System.out.println("Sending SMS: " + message);
}
}
// 고수준 클래스는 추상화에 의존
public class NotificationService {
private MessageSender messageSender;
// 의존성 주입을 통해 유연성 확보
public NotificationService(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void notify(String message) {
messageSender.sendMessage(message);
}
}
// 사용 예제
public class Main {
public static void main(String[] args) {
// 이메일로 알림
NotificationService emailNotification =
new NotificationService(new EmailSender());
emailNotification.notify("Hello via Email");
// SMS로 알림 - 코드 변경 없이 교체 가능!
NotificationService smsNotification =
new NotificationService(new SMSSender());
smsNotification.notify("Hello via SMS");
}
}
6. SOLID 장점
- 유지보수성 향상 / 확장성 / 테스트 용이성 / 코드 재사용성 / 팀 협업
IoC/DI
- 의존성: 한 클래스가 다른 클래스(혹은 객체)를 사용하는 관계
- IoC: 스프링이 대신 객체 생성
- DI: 스프링이 개발자 대신 생성한 객체를 관리하고 있는데, 그 객체(=의존성)를 가져와서 사용(주입)
Bean
1. IoC Container와 Bean
1) Bean: 스프링이 관리하는 객체
- Spring 컨테이너에 의해 생성, 관리, 소멸됨
- 애플리케이션 전역에서 재사용 가능
- 기본적으로 싱글톤 스코프로 관리
2) 싱글톤
- 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
- 애플리케이션 전체에서 해당 클래스의 객체를 하나만 만들고, 그것을 공유해서 사용
- 사용하는 이유: 메모리 효율성 / 데이터 공유와 일관성
3) 스프링 IoC Container
- Bean의 생성 및 생명주기 관리
- 의존성 주입 (DI)
- Bean 설정 정보 관리
- Bean 간의 의존 관계 설정

4) new VS IoC/DI
- new: 매번 새로운 상태를 가지는 데이터 객체
- IoC/DI: 재사용되거나 교체 가능한 비즈니스 로직/인프라
2. 어노테이션 합성
- 여러 개의 어노테이션을 조합하여 하나의 새로운 어노테이션을 만드는 것
3. 스프링 Bean 등록
1) 자동 등록
- @Component 클래스를 찾아서 Bean으로 등록하는 방식
- @ComponentScan에 설정되어있는 패키지를 기준으로 하위의 모든 @Component 클래스를 탐색하여 Bean으로 등록
- @Component: 붙은 클래스는 Bean으로 등록됩니다 ( @Controller(@RestController), @Service, @Repository)
- @ComponentScan: 어디에서 빈으로 사용할 클래스들을 찾아야 할지 알려주는 안내자 역할 / 지정된 경로와 그 하위 패키지를 모두 탐색 > @SpringBootApplication 어노테이션에 이미 포함
2) 수동 등록
- @Configuration 클래스와 @Bean 메소드를 사용하여 명시적으로 Bean을 등록하는 방식
- @Configuration: 스프링 빈을 어떻게 만들지 적어놓은 설정 클래스 / @Bean 메소드들이 Bean 정의를 포함
// 무선마우스로 사용
@Configuration
class AppConfig {
@Bean
Mouse mouse() {
return new WirelessMouse();
}
}
4. DI 방식 비교
1) 필드 주입
- @Autowired를 필드에 직접 선언하여 의존성을 주입받는 방식
- final X > 테스트 코드에서만 사용
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepository;
}
2) 생성자 주입
- 생성자를 통해 의존성을 주입받는 방식
- final O > 불변성 보장
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired // 생성자가 1개만 있으면 @Autowired 생략 가능
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
5. Bean 우선순위 : 빈이 2개 일 때
@Component
public class KakaoPayService implements PayService { ... }
@Component
public class NaverPayService implements PayService { ... }
1) 해결 1: @Primary
- 우선적으로 선택될 기본(Default) 빈을 지정하는 어노테이션
@Component
public class KakaoPayService implements PayService { ... }
@Primary // PayService 타입의 빈이 여러 개일 때, 우선권을 갖는다!
@Component
public class NaverPayService implements PayService { ... }
@RestCotroller
@RequiredArgsConstructor
public class PayController {
private final PayService payService; // NaverPayService가 자동으로 주입됨
// ...
}
2) 해결 2: @Qualifier
- 특정 빈을 직접 지정하여 주입하는 어노테이션
@Component("kakaoPay") // "kakaoPay"라는 별명을 붙여줌
public class KakaoPayService implements PayService { ... }
@Component("naverPay")
public class NaverPayService implements PayService { ... }
@Service
public class OrderService {
private final PayService payService;
// "kakaoPay"라는 이름을 가진 빈을 주입해줘!
public OrderService(@Qualifier("kakaoPay") PayService payService) {
this.payService = payService;
}
}
6. Bean 스코프
- 스프링 빈이 얼마나 오래, 그리고 어떻게 존재할지를 정의하는 개념
- 기본(Default) 값은 싱글톤 > 외에 잘 사용 X
- 싱글톤 스코프: 단 한 번만 생성되고, 애플리케이션이 끝날 때까지 계속 재사용
- 프로토타입 스코프: 요청이 올 때마다 계속 새로운 객체를 생성하여 반환
7. 라이프사이클 콜백

- 생성 → 의존성 주입 → 초기화 → 사용 → 소멸
- 초기화와 소멸 단계에서 특정 작업을 수행하도록 콜백(Callback) 메서드를 지정
- 초기화 단계: @PostConstruct 어노테이션이 붙은 메서드는 빈의 생성과 모든 의존성 주입이 완료된 직후에 딱 한 번 호출
의존성을 사용하여 외부 리소스를 가져오거나 초기 설정 값을 세팅하는 등 무거운 초기화 작업에 사용 - 소멸 단계: @PreDestroy는 스프링 컨테이너에서 빈이 제거되기 직전에 호출
뒷정리 작업에 사용
@Component
public class MusicPlayer {
private List<String> playlist = new ArrayList<>();
// 초기화 콜백: 의존성 주입이 끝난 후 실행
@PostConstruct
public void loadPlaylist() {
System.out.println("--- @PostConstruct 호출 ---");
playlist.add("아이유 - 라일락");
playlist.add("BTS - Dynamite");
System.out.println("플레이리스트 로딩 완료!");
}
// 소멸 전 콜백: 빈이 사라지기 직전 실행
@PreDestroy
public void saveProgress() {
System.out.println("--- @PreDestroy 호출 ---");
System.out.println("뮤직 플레이어를 종료합니다...");
}
}
Validation
1. 개념
- 특정 데이터의 값이 유효한지 확인하는 단계
- 시스템이 미리 정의한 사양에 부합하고 있는지 검증
2. 역할
- 검증을 통해 적절한 메세지를 유저에게 보임
- 검증 오류로 인해 정상적인 동작을 못하는 경우는 X
- 사용자가 입력한 데이터는 유지된 상태
3. 검증 종류
1) 프론트엔드 검증
- 해당 검증은 유저가 조작할 수 있음으로 보안에 취약
- 보안에 취약하지만 그럼에도 꼭 필요
2) 서버 검증
- 프론트 검증없이 서버에서만 검증한다면 유저 사용성이 떨어짐
- API 스펙을 정의해서 Validation 오류를 Response 예시에 남겨야 함 > API 명세서 잘 만들어야 함
- 서버 검증은 선택이 아닌 필수
3) 데이터베이스 검증
- Not Null, Default와 같은 제약조건을 설정
- 최종 방어선의 역할을 수행
4. Bean Validation
- 검증 로직을 어노테이션으로 표현하는 기술
- 코드 예시
더보기
- 의존성
implementation 'org.springframework.boot:spring-boot-starter-validation'
- 코드 예시
public class SaveMemberRequestDto {
@NotBlank // 1. "값이 꼭 있어야 해요!" (null, "", " " 모두 거부)
private String name;
@Email // 2. "이메일 형식이어야 해요!" (xxx@xxx.xxx)
private String email;
@Size(min = 8, max = 20) // 3. "8~20자 사이여야 해요!"
private String password;
@Min(19) // 4. "최소 19 이상이어야 해요!"
private Integer age;
@Pattern(regexp = "^010-\\d{4}-\\d{4}$") // 5. "이 패턴과 일치해야 해요!"
private String phone;
}
- 많이 쓰이는 어노테이션:
@NotBlank: 공백이 아닌 문자가 1개 이상 (null, "", " " 모두 거부)
@Email: @가 포함된 이메일 형식
@Size: 문자열 길이나 컬렉션 크기 제한
@Min/@Max: 숫자의 최소/최대값
@Pattern: 정규표현식 패턴 (전화번호, 주민번호 형식 등) - 컨트롤러에서 검증 실행하기
더보기
- @Valid만 붙이면 자동으로 검증
@RestController
public class MemberController {
// @Valid만 붙이면 자동으로 검증 실행!
@PostMapping("/signup")
public String signup(@Valid @RequestBody SaveMemberRequestDto request) {
// ...
return "가입이 완료되었습니다.";
}
}
@Getter
public class SaveMemberRequestDto {
@NotBlank // 1. "값이 꼭 있어야 해요!" (null, "", " " 모두 거부)
private String name;
@Email(message = "올바른 이메일 형식이 아닙니다.") // 2. "이메일 형식이어야 해요!" (xxx@xxx.xxx)
private String email;
@Size(min = 8, max = 20) // 3. "8~20자 사이여야 해요!"
private String password;
@Min(19) // 4. "최소 19 이상이어야 해요!"
private Integer age;
@Pattern(regexp = "^010-\\d{4}-\\d{4}$") // 5. "이 패턴과 일치해야 해요!"
private String phone;
}
5. 정규식
- 문자열의 패턴을 표현하는 특별한 문자 조합
- 정규표현식, Regex
- 필요성: 코드예시
더보기
- 정규식X
public boolean isValidPhone(String phone) {
if (phone == null) return false;
if (phone.length() != 13) return false;
if (!phone.startsWith("010")) return false;
if (phone.charAt(3) != '-') return false;
if (phone.charAt(8) != '-') return false;
String[] parts = phone.split("-");
try {
Integer.parseInt(parts[1]);
Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
return false;
}
return true;
}
- 정규식O
public boolean isValidPhone(String phone) {
return phone.matches("^010-\\d{4}-\\d{4}$");
}
예외처리
1. 스프링 예외처리 전략
1) 스프링 예외 처리 전략 3가지
- @ExceptionHandler
- @RestControllerAdvice
- Spring 기본 예외 처리
2) @ExceptionHandler - 컨트롤러별 예외 처리
- 개별적으로 컨트롤러마다 설정하는 방법
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/members/{memberId}")
public ResponseEntity<GetMemberResponse> getMember(
@PathVariable Long memberId
) {
return ResponseEntity.status(HttpStatus.OK).body(memberService.findOne(memberId));
}
// 🚨 예외 처리 메서드
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<String> handlerIllegalStateException(IllegalStateException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("요청 오류: " + e.getMessage());
}
}
3) @RestControllerAdvice - 전역 예외 처리
- 모든 컨트롤러의 예외를 한 곳에서 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
// 🎯 커스텀 비즈니스 예외 처리
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<String> handleIllegalStateException(IllegalStateException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("요청 오류: " + e.getMessage());
}
}
2. 로깅
1) 로그
- 프로그램이 예상대로 잘 동작하는지, 혹은 문제가 생겼을 때 어디서 왜 문제가 발생했는지 추적하기 위해 기록
- 로깅: 기록을 남기는 행위
2) 로그 레벨 이해하기
// 로그 레벨 낮음 -> 높음
TRACE → DEBUG → INFO → WARN → ERROR
3) 로그 레벨 설정
- application.properties에서 로그 레벨을 설정
// 소문자로도 설정 가능!
logging.level.root=WARN
- 아무 것도 설정하지 않았을 때 기본 로그 레벨은 INFO
4) 로깅
- Lombok 라이브러리의 @Slf4j 어노테이션을 사용 > log.xxx 형태 = 간편
- 로깅 예시
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@Slf4j // <-- 바로 이 어노테이션입니다!
@RestController
public class LogController {
@GetMapping("/log-test/{userId}")
public String logTest(@PathVariable String userId) {
String userName = "봉이 김선달";
// 각 로그 레벨별로 메시지를 출력합니다.
log.trace("TRACE 로그: 사용자 ID = {}, 사용자 이름 = {}", userId, userName);
log.debug("DEBUG 로그: 사용자 ID = {}, 사용자 이름 = {}", userId, userName);
log.info("INFO 로그: 사용자 ID = {} 님이 로그인했습니다.", userId);
log.warn("WARN 로그: 사용자 ID = {} 님의 비밀번호 만료가 임박했습니다.", userId);
try {
// 일부러 예외를 발생시킵니다.
int errorResult = 10 / 0;
} catch (Exception e) {
log.error("ERROR 로그: 사용자 ID = {} 님 처리 중 예외 발생!", userId, e);
}
return "로그 테스트 완료!";
}
}
- System.out.println 사용 X
3. 커스텀 에러
1) 커스텀 에러 만들기
더보기
- RuntimeException 상속받기 > 예외 클래스 만들기
// extends RuntimeException 중요!
public class MovieNotFoundException extends RuntimeException {
public MovieNotFoundException(String message) {
super(message);
}
}
- 커스텀 에러 쓰기
// Service 코드
@Transactional(readOnly = true)
public GetMovieResponse findOne(Long movieId) {
Movie movie = movieRepository.findById(movieId).orElseThrow(
() -> new MovieNotFoundException("없는 영화입니다.")
);
return new GetMovieResponse(
movie.getId(),
movie.getTitle(),
movie.getEmail(),
movie.getPassword(),
movie.getPhoneNumber()
);
}
- GlobalExceptionhHandler로 커스텀 에러를 핸들링 하기
@RestControllerAdvice
public class GlobalExceptionHandler {
// MovieNotFoundException 커스텀 에러 핸들링
@ExceptionHandler(MovieNotFoundException.class)
public ResponseEntity<ErrorResponse> handleMovieNotFoundException(MovieNotFoundException ex) {
ErrorResponse response = new ErrorResponse(ex.getMessage());
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response);
}
}
- 문제점: 커스텀 에러 생길 때마다 GlobalExceptionHandler 하나하나 만들어야 함 > 공통된 부모 에러 타입 만들기
2) 공통 부모 커스텀 에러 타입 만들기
더보기
- 공통 상위 커스텀 에러 클래스 만들기
import lombok.Getter;
@Getter
public class ServiceException extends RuntimeException {
private final HttpStatus status;
public ServiceException(HttpStatus status, String message) {
super(message);
this.status = status;
}
}
- MovieNotFoundException을 작성
// extends ServerException 중요!
public class MovieNotFoundException extends ServiceException {
public MovieNotFoundException(String message) {
super(HttpStatus.NOT_FOUND, message); // HttpStatus.NOT_FOUND 지정
}
}
- 새로 만든 커스텀 에러 쓰기
// Service 코드
@Transactional(readOnly = true)
public GetMovieResponse findOne(Long movieId) {
Movie movie = movieRepository.findById(movieId).orElseThrow(
() -> new MovieNotFoundException("없는 영화입니다.")
);
return new GetMovieResponse(
movie.getId(),
movie.getTitle(),
movie.getEmail(),
movie.getPassword(),
movie.getPhoneNumber()
);
}
- GlobalExceptionhHandler로 커스텀 에러들을 단 하나의 메서드로 모두 핸들링 가능
@RestControllerAdvice
public class GlobalExceptionHandler {
// MovieNotFoundException 커스텀 에러 핸들링
@ExceptionHandler(ServiceException.class)
public ResponseEntity<String> handleServiceException(ServiceException ex) {
return ResponseEntity
.status(ex.getStatus())
.body(ex.getMessage());
}
}
- 예시) 이메일 중복 에러 / 에러코드 400\
// extends ServerException 중요!
public class DuplicateEmailException extends ServiceException {
public DuplicateEmailException(String message) {
super(HttpStatus.BAD_REQUEST, message);
}
}
4. Bean Validation 에러 핸들링
- 클라이언트에게 에러 보여주지 X
- Bean Validation은 내부적으로 MethodArgumentNotValidException 에러 발생
- GlobalExceptionHandler 클래스에 해당 에러를 핸들링 > 클라이언트에게 원하는 에러 메시지 보여줌
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.findFirst() // 첫 번째 에러를 Optional로 가져옴
.map(fieldError -> fieldError.getDefaultMessage()) // 있다면 메시지로 변환
.orElse("입력 값이 올바르지 않습니다."); // 없다면 기본 메시지 사용
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMessage);
}
- 에러 2개 이상 O
'🔌 SPARTA > Courses' 카테고리의 다른 글
| [숙련 Spring] Spring Data JPA (0) | 2026.02.18 |
|---|---|
| [숙련 Spring] Spring Data JPA (0) | 2026.02.11 |
| 스탠다드반 3회차: 실전 SQL (0) | 2026.02.09 |
| 스탠다드반 2회차: 데이터 베이스 모델링 (0) | 2026.02.05 |
| 스탠다드반 1회차: MVC & REST API (0) | 2026.02.02 |