1. 런타임 환경 안정성 및 일반적 오류 처리
Spring 기반 웹 애플리케이션을 개발할 때 발생하는 환경 관련 오류와 코드 레벨의 빈티지한 버그를 식별하는 것이 중요합니다.
토마캣 포트 충돌 문제
IDE 의 비정상 종료로 인해 토마캣 프로세스가 제대로 종료되지 않고 남아있을 경우, 재시작 시 BindException: Address already in use 오류가 발생할 수 있습니다. 단순히 컴퓨터 재부팅보다는 시스템의 작업 관리자에서 해당 포트를 사용하는 자바 프로세스를 명시적으로 종료하거나, 개발 서버 설정상 디버깅 포트 주기를 확인하여 충돌을 예방하는 것이 바람직합니다.
예: 디버깅 포트 충돌 시 메시지<br>Error running 'Project': Unable to open debugger port (127.0.0.1:XXXXX):<br>java.net.BindException: Address already in use
제네릭 타입 안전성과 널 체크
기본 타입 (int) 과 참조 타입 (Integer 또는 String) 을 비교 연산자를 사용하여 검사할 때, 자동 언박싱 과정에서의 NullPointerException(NPE) 발생 가능성이 높습니다.
특히 사용자 입력 데이터를 매개변수로 받거나 DB 에서 조회한 값일 경우 null 인 상태를 고려하지 않으면 런타임 오류로 이어집니다. 따라서 상수나 고정된 문자열을 먼저 배치하여 equals 메서드를 호출하는 패턴을 권장합니다.
// 리팩토링 전: Null Pointer 위험
// int value = 2; Integer input = null;
// if (value == input) { ... } // NPE 발생 가능
// 리팩토링 후: 안전한 비교
if ("expected_value".equals(actualValue)) {
// 처리 로직
}
2. 웹 프로젝트 구조 및 클래스패스 관리
자바 웹 표준 규격인 Servlet Specification 에 따라 외부 의존성 라이브러리의 위치는 엄격히 규정되어 있습니다.
WEB-INF/lib 의 중요성
서드파티 JAR 파일은 반드시 WEB-INF/lib 하위에 배치되어야 합니다. 이를 무시하고 다른 이름 (예: libs) 의 폴더를 사용할 경우, 일부 IDE 는 빌드 과정에서 경로를 인식하지 못하여 NoClassDefFoundError 를 유발합니다.
- 원본 소스는
src/main/java에서 컴파일되어WEB-INF/classes로 이동합니다. - 라이브러리는 배포 시점에
WEB-INF/lib내에 있어야 컨텍스트 로더에서 정상 로드됩니다.
리소스 로드 전략 (ClassLoader vs Class)
설정 파일이나 자원을 로드할 때 경로 해석 방식에 유의해야 합니다. ClassLoader 와 Class 객체의 메서드 사용법은 차이가 있습니다.
- ClassLoader.getResource(String): 클래스패스 루트를 기준으로 하므로 경로 앞에 슬래시 (
/) 를 붙이면 오히려null이 반환될 수 있습니다. - Class.getResource(String): 절대경로 (클래스패스 루트 기준) 에는 앞뒤 슬래시가 필요하며, 상대경로는 현재 클래스 위치를 기준으로 합니다.
public class ResourcePathChecker {
public void loadProperties() throws IOException {
// ClassLoader 활용: 클래스패스 루트 기반 (슬래시 제외 권장)
InputStream stream1 = this.getClass().getClassLoader()
.getResourceAsStream("application.properties");
// Class 활용: 절대 경로 지정 시 슬래시 필요
InputStream stream2 = this.getClass()
.getResourceAsStream("/config/db-config.xml");
// 실행 시 실제 물리 경로 확인용
URL classpathUrl = this.getClass().getResource("/");
System.out.println("Runtime Path: " + classpathUrl.getPath());
}
}
3. Spring MVC 동작 원리 및 아키텍처
Spring MVC 는 Front Controller 패턴을 기반으로 하여 모든 요청을 중앙 집중식으로 처리합니다.
- 클라이언트의 HTTP 요청이 DispatcherServlet 으로 진입합니다.
- DispatcherServlet 은 HandlerMapping 을 통해 요청 URL 이 해당하는 컨트롤러 핸들러를 찾습니다.
- 필요하다면 등록된 Interceptor 가 요청을 가로채어 전처리 작업을 수행합니다.
- HandlerAdapter 가 특정 컨트롤러 메서드를 invokes 하여 비즈니스 로직을 실행합니다 (Service -> DAO).
- 컨트롤러는 결과를
ModelAndView로 반환합니다. - ViewResolver 가 뷰 이름을 실제 JSP, Thymeleaf 템플릿 등으로 resolved 합니다.
- 최종 응답이 클라이언트로 전송됩니다.
4. 데이터 검증: @Valid 와 @Validated 구분
JSR-303 표준 검증 어노테이션과 Spring 특화 어노테이션은 용도에 따라 선택해야 합니다.
@Valid
표준 검증 규격을 따르는 어노테이션입니다. 주로 메서드 파라미터에 적용하여 객체 내부 필드의 유효성을 기본값으로 확인합니다. 그룹화를 지원하지 않습니다.
@PostMapping("/register")
public ResponseEntity createMember(@Valid RegisterForm form) {
// form 내부 필드 검증 실행
return ResponseEntity.ok("Created");
}
@Validated
Spring Framework 에서 제공하는 확장형 어노테이션으로, 검증 그룹핑 기능을 지원합니다. 생성과 수정 시 서로 다른 검증 규칙이 필요한 경우에 유용합니다.
public interface CreateGroup {}
public interface UpdateGroup {}
@PostMapping("/members")
public ResponseEntity save(@Validated(CreateGroup.class) MemberDto dto) {
// 생성 전용 규칙만 적용
memberService.save(dto);
return ResponseEntity.ok("Saved");
}
@PutMapping("/members/{id}")
public ResponseEntity update(@Validated(UpdateGroup.class) @PathVariable Long id, MemberDto dto) {
// 수정 전용 규칙만 적용 (예: 아이디 불변 등)
memberService.update(id, dto);
return ResponseEntity.ok("Updated");
}
5. 인터셉터 등록 방식 및 생명주기
인터셉터를 IoC 컨테이너에 등록하는 방법은 크게 두 가지 접근이 가능합니다.
방식 A: 직접 인스턴스 생성 (Manual Instantiation)
설정 클래스 내부에서 new 키워드로 직접 객체를 만들어 등록합니다. 이 경우 인터셉터 클래스 자체에는 @Component 선언이 불필요합니다. 스프링의 생명주기가 관리되지 않으므로 간단한 필터 역할을 할 때 적합합니다.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 직접 생성하여 등록
registry.addInterceptor(new AuthCheckInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/css/**");
}
}
// 별도의 @Component 제거 권장
public class AuthCheckInterceptor implements HandlerInterceptor {
// 구현...
}
방식 B: 의존성 주입 활용 (DI Support)
인터셉터 내부에서도 Spring Bean 들을 주입받거나 라이프사이클 콜백이 필요하다면 @Component 로 선언하고 설정 클래스에서 주입받도록 작성합니다.
@Configuration
public class AdvancedMvcConfig implements WebMvcConfigurer {
private final SecurityInterceptor securityInterceptor;
// 생성자 주입
public AdvancedMvcConfig(SecurityInterceptor securityInterceptor) {
this.securityInterceptor = securityInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 컨테이너 관리되는 Bean 등록
registry.addInterceptor(securityInterceptor)
.addPathPatterns("/api/**");
}
}
@Component
public class SecurityInterceptor implements HandlerInterceptor {
// 내부 로직
}