2026.04.16
응집도와 결합도는 유지보수하기 좋고 유연한 코드를 작성하기 위한 매우 중요한 개념임!
클래스 내의 메소드와 데이터들이 얼마나 서로 관련 있는 작업을 수행하는지를 의미하는 것. (좋은 소프트웨어를 위해서는 응집도를 높여줘야함!)
응집도를 높이기 위해서는 하나의 클래스는 하나의 책임만을 지게 해야함.
얼마나 의존적인지를 나타내는 척도. (좋은 소프트웨어를 위해서는 결합도를 느슨하게 만들어줘야함!)
결합도를 느슨하게 만들기 위해서는 하나의 클래스가 변경되어도 다른 클래스에 미치는 영향이 거의 없도록 설계해야함.
유지보수과 확장성을 위해서 이해하기 편하고 유연한 소프트웨어를 만들기 위한 다섯가지 객체 지향 설계 원칙.
지금까지는 개발자가 코드 내에서 사용할 객체를 직접 생성하고 관리했지만, 스프링에서는 객체의 생성과 관리를 스프링이 해줌. (개발자가 new 키워드로 객체를 생성하지 않아도 된다는 뜻!)
DI는 IoC 개념을 실제로 구현하는 것.
스프링이 개발자 대신 생성하고 관리하고 있는 그 객체(=의존성)를 가져와서 사용(주입)하는 것.
IoC를 통해서 스프링이 개발자를 대신해서 객체를 생성하고 관리를 하는데, 여기서 생성되는 객체를 Bean
이렇게 생성된 Bean 을 담아서 보관하고 있는 상자가 IoC Container
IoC Container 는 Bean 을 생성하고, 생명주기를 관리하고, 의존성을 주입(DI)함.
Bean 은 기본적으로 메모리의 효율성과 데이터 공유와 일관성을 위해서 싱글톤 스코프로 관리
싱글톤(Singleton) 은 클래스의 인스턴스가 딱 1개만 생성되는 것.
new 키워드를 스프링이 대신 사용해서 객체를 만들어준다고 했지만, 앞에 강의나 과제를 하면서 new 를 몇 번 사용한 적이 있었음.
new 는 안 쓰는거 아니었나? 했지만, 사용해야하는 때가 있음.
new를 안 쓰는 경우)“재사용되거나 교체 가능한 비즈니스 로직/인프라” 에서는 IoC/DI를 통해 자동으로 생성되는 객체를 사용하면 됨.
예를 들면, @Service , @Repository , @Component , @Controller 같은 어노테이션이 붙은 것들.
new 를 사용해야하는 경우“매번 새로운 상태를 가지는 데이터 객체” 에서는 개발자가 new 를 사용해서 생성을 해줌. (해당 로직 안에서만 필요한 객체이거나 하면 그 로직 안에서 만들고 사용하고 그 로직이 끝나면 그냥 버려지는 듯?)
@Service , @Repository , @Component , @Controller 같은 어노테이션이 붙지 않은 객체들은 new 를 사용해서 생성해줘야함.
여러개의 어노테이션을 조합해서 하나의 새로운 어노테이션을 만드는 것.
입문 Spring 과제를 진행하면서 등장했던 친구가 @Controller + @ResponseBody = @RestController ← 얘임.
스프링에서는 Bean 등록을 자동 등록과 수동 등록 방식이 있음.
스프링이 @Component 를 찾아서 자동으로 Bean으로 등록을 해줌.
@ComponentScan 에 설정되어있는 패키지를 기준으로 하위의 모든 @Component 클래스를 탐색해서 Bean으로 등록.
이렇게 자동으로 등록을 할려면 꼭 @Component 어노테이션을 달아줘야함.
@Configuration 클래스와 @Bean 메소드를 사용해서 명시적으로 Bean을 등록.
@Configuration 클래스 → @Bean 메소드 → Spring IoC 컨테이너에 Bean 으로 등록.
@Autowired 를 필드에 직접 선언해서 의존성을 주입받는 방식.
이 방법에는 여러 문제들이 있지만, 대표적으로 final 키워드를 사용할 수 없어서, 안정성이 떨어지기 때문에 더 이상 사용하지 않음.
Setter 메서드 를 통해서 의존성을 주입받는 방식.
필드 주입 방식과 같은 문제가 있어서 안전하지 않기 때문에 더 이상 사용하지 않음.
생성자를 통해서 의존성을 주입받는 방식. (권장)
지금까지 계속 사용해오던 방식으로, 생성자를 작성해서 주입하는 것인데 Lombok의 @RequiredArgsConstructor 를 사용해서 더 간결하게 작성 할 수 있음.
final 로 선언해서 런타임에 의존성이 변경될 위험이 없음. → 불변성(Immutability) 보장
만약에 스프링 컨테이너에 똑같은 타입의 Bean이 2개 이상 등록이되면 어떻게 될까?
@Component
public class KakaoPayService implements PayService { ... }
@Component
public class NaverPayService implements PayService { ... }
예를 들어서 이 코드에 두 Bean은 PayService라는 공통적인 부모 클래스를 상속받고 있는데, 이 두 Bean에게 이름을 지어주지 않으면
@Service
@RequiredArgsConstructor
public class OrderService {
// 카카오페이가 주입될까, 아니면 네이버페이가 주입될까?
private final PayService payService;
// ...
}
여기서 PayService를 의존성 주입(DI)을 하게 되면 어떤 친구가 주입이 될까?
PayService라는 Bean이 2개 이상 존재하고 있기 때문에 에러가 발생함.
@Primary 는 여러 빈 중에서 우선적으로 선택될 기본 빈을 지정하는 어노테이션.
@Qualifier 라는 이름으로 특정 빈을 직접 지정해서 주입하는 어노테이션.
@Primary 보다 우선순위가 높고, 더 구체적인 선택이 필요할 때 사용.
이 어노테이션을 사용하면 생성자를 자동으로 생성해주는 @RequiredArgsConstructor 를 사용할 수 없어서 생성자를 직접 입력 해줘야함!
Bean 스코프는 스프링 빈이 얼마나 오래, 그리고 어떻게 존재할지를 정의하는 개념. 기본값은 싱글톤!
스프링 컨테이너가 시작될 때 딱 1번만 생성되고, 애플리케이션이 끝날 때까지 계속 재사용 되는 방식. (대부분의 빈은 싱글톤으로 관리됨.)
@Scope(”prototype”) 으로 지정된 빈은, 요청이 올 때마다 계속 새로운 객체를 생성해서 사용되는 방식.

Controller의 주요 역할 중 하나로, 특정 데이터(주로 클라이언트의 요청 데이터)의 값이 유효한지 확인(HTTP 요청이 정상인지 검증)하는 단계.
이런 검증들을 꼼꼼하게 하는 것이 수많은 Error와 문제들을 방지 할 수 있음.
웹에서 가장 중요한 것 중 하나가 잘못된 데이터가 시스템으로 들어오지 못하게 막는 것.
이런 로직을 어노테이션으로 편하고 직관적으로 데이터를 검증 할 수 있음.(만약에 Bean Validation이 없으면 if문으로 하나하나 검증을 해줘야함.)
📌많이 쓰이는 Validation 어노테이션
@NotBlank : 공백이 아닌 문자가 1개 이상 (null, “”, “ “ 모두 거부)@Email : @가 포함된 이메일 형식@Size : 문자열 길이나 컬렉션 크기 제한@Min/@Max : 숫자의 최소/최댓값@Pattern : 정규표현식 패턴 (전화번호, 주민번호 형식 등)문자열의 패턴을 표현하는 특별한 문자 조합, (”이런 형식이어야 해!” 라고 하는 것)
정규표현식, Regex 라고도 함.
예를 들면 휴대폰 번호 형식같은 것들을 검증 할 때 사용 할 수 있음.
정규식 만드는 방법은 존재는 하지만 공부할 필요는 음슴..ㅋㅋㅋ 구글링 하면 다 나옴.
ex) 이메일 형식 정규식 등등..
자바에서는 try-catch문을 사용해서 예외처리를 했지만, 스프링에서는 다르게 함.
📌스프링 예외처리 전략 3가지
동시에 존재할 경우 → 번호 순서대로 우선순위를 가짐.
컨트롤러별로 발생하는 예외만을 처리
하지만, 매번 개별적인 컨트롤러 마다 @ExceptionHandler를 설정하는 방법은 중복이 심해지기 때문에 잘 사용하지 않음.
모든 컨트롤러의 예외를 한 곳에서 처리 (가장 많이 사용되는 방식으로 사실상 표준!)
@RestControllerAdvice
public class GlobalExceptionHandler {
// 🎯 커스텀 비즈니스 예외 처리
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<String> handleIllegalStateException(IllegalStateException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("요청 오류: " + e.getMessage());
}
}
프로그램이 예상대로 잘 동작하는지, 혹은 문제가 생겼을 때 어디서 왜 문제가 발생했는지 추적하기 위한 기록을 로그(Log)
이러한 기록을 남기는 행위를 로깅(Logging)
application.properties에서 로그 레벨을 설정해줄 수 있음
// 소문자로도 설정 가능!
logging.level.root=WARN
아무것도 설정하지 않았을 때 기본 로그 레벨은 INFO (특별한 경우가 아니면 보통 INFO로 사용함)
스프링 프레임워크에서는 Lombok 라이브러리의 @Slf4j 어노테이션을 사용해서 Log.xxx 의 형태로 간편하게 로깅 가능.
System.out.println 은 로깅 프레임워크보다 성능이 좋지 않아서 사용하지 않는 것이 좋음.지금까지 예외를 던질 때, IllegalStateException 같은 예외를 던졌음. 하지만, 이런 예외는 너무 범용적이라서 나중에 코드를 볼 때 이게 왜 발생했는지 한눈에 파악하기 어려움.
RuntimeException 을 상속받아서 나만의 클래스를 만들 수 있음. (반드시 RuntimeException 을 상속받아야함.)
// extends RuntimeException 중요!
public class MovieNotFoundException extends RuntimeException {
public MovieNotFoundException(String message) {
super(message);
}
}
이렇게 나만의 유니크한 예외를 만들 수 있음~
하지만, 이렇게 예외를 수백 수천가지를 만들어줄 수 없기 때문에 공통된 부모 에러 타입을 하나 더 만들면 좋음.
약간 범용적으로 사용할 예외를 만드는 것임.
import lombok.Getter;
@Getter
public class ServiceException extends RuntimeException {
private final HttpStatus status;
public ServiceException(HttpStatus status, String message) {
super(message);
this.status = status;
}
}
이 부모 클래스를 기준으로 다시 커스텀 에러를 만들어보면
// extends ServerException 중요!
public class MovieNotFoundException extends ServiceException {
public MovieNotFoundException(String message) {
super(HttpStatus.NOT_FOUND, message); // HttpStatus.NOT_FOUND 지정
}
}
@RestControllerAdvice
public class GlobalExceptionHandler {
// MovieNotFoundException 커스텀 에러 핸들링
@ExceptionHandler(ServiceException.class)
public ResponseEntity<String> handleServiceException(ServiceException ex) {
return ResponseEntity
.status(ex.getStatus())
.body(ex.getMessage());
}
}
커스텀 에러들을 단 하나의 메서드로 모두 핸들링 할 수 있음.
앞에서 Bean Validation을 배웠지만, 그 에러를 클라이언트에게 보여줄 수는 없었음.
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);
}
예외처리를 하는 부분 커스텀 에러를 만들어서 사용을 하는 부분.
갑자기 왜 그것보다 더 범용적인 클래스를 하나 더 만들어서 사용하는지 이해하기 어려웠음.
예외가 수백 수천가지씩 늘어날수록 Handler에서도 계속해서 예외를 추가해줘야하는 문제 때문에 많은 예외들을 부모클래스로 묶어놓고 Handler에서 그거 하나만을 가지고 돌아가는 거였음.