Today's Codekata
// 콜라 문제
class Solution {
public int solution(int a, int b, int n) {
int answer = 0;
while (n >= a) {
int newCola = (n / a) * b;
answer += newCola;
n = (n % a) + newCola;
}
return answer;
}
}
// 명예의 전당
class Solution {
public int[] solution(int k, int[] score) {
int[] answer = new int[score.length];
List<Integer> honor = new ArrayList<>();
for (int i = 0; i < score.length; i++) {
honor.add(score[i]);
honor.sort((a, b) -> a - b);
if (honor.size() > k) {
honor.remove(0);
}
answer[i] = honor.get(0);
}
return answer;
}
}
for문 안에 for문을 넣는 것이 약간 습관이 된 것 같다. 그럼으로 인해서 for문 내부에서 어느 위치에 있어야 내가 원하는 순서로 로직이 돌아가는지 스스로 헷갈리게 만드는 느낌이다. 리스트에서 `list.sort((a,b) -> a-b);` 는 오름차순 b-a를 넣어주면 내림차순이 된다.
-- 식품분류별 가장 비싼 식품의 정보 조회하기
SELECT FP.CATEGORY, FP.PRICE AS MAX_PRICE, FP.PRODUCT_NAME
FROM FOOD_PRODUCT FP
INNER JOIN (
SELECT CATEGORY, MAX(PRICE) AS MAX_PRICE
FROM FOOD_PRODUCT
WHERE CATEGORY IN ('과자', '국', '김치', '식용유')
GROUP BY CATEGORY
) A ON FP.CATEGORY = A.CATEGORY AND FP.PRICE = A.MAX_PRICE
ORDER BY FP.PRICE DESC;
-- 5월 식품들의 총매출 조회하기
SELECT FP.PRODUCT_ID, FP.PRODUCT_NAME, SUM(FP.PRICE * FO.AMOUNT) AS TOTAL_SALES
FROM FOOD_PRODUCT FP
INNER JOIN FOOD_ORDER FO ON FP.PRODUCT_ID = FO.PRODUCT_ID
WHERE FO.PRODUCE_DATE BETWEEN '2022-05-01' AND '2022-05-31'
GROUP BY 1, 2
ORDER BY 3 DESC, 1
-- 없어진 기록 찾기
SELECT AO.ANIMAL_ID, AO.NAME
FROM ANIMAL_INS AI
RIGHT JOIN ANIMAL_OUTS AO ON AI.ANIMAL_ID = AO.ANIMAL_ID
WHERE AI.ANIMAL_ID IS NULL
ORDER BY AO.ANIMAL_ID
서브쿼리 JOIN 방식은 특정 집계 결과나 조건을 선별하고, 이를 기반으로 메인 테이블과 JOIN하여 최종 데이터를 조회한다.
Today I Learned
일정 관리 앱 마무리
Spring Boot Validation
Spring boot에서는 Bean Validation을 기반으로 유효성 검사를 수행한다.
| 주요 어노테이션 | 설명 |
| @NotNull | null이 아니어야 함 |
| @NotBlank | null, 공백, 빈 문자열 모두 허용하지 않음 |
| @Size(min, max) | 문자열, 컬렉션 등의 크기 제한 |
| 이메일 형식 검증 | |
| @Pattern(regexp) | 정규식 패턴 검증 |
| @Min, @Max | 숫자 범위 제한 |
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
public class EventRequestDto {
@NotBlank(message = "제목은 필수입니다.")
@Size(max = 30)
private String title;
@NotBlank(message = "내용은 필수입니다.")
@Size(max = 200)
private String description;
@NotBlank(message = "이름은 필수입니다.")
private String name;
@NotBlank(message = "비밀번호는 필수입니다.")
private String password;
}
먼저 build.gradle에 의존성을 추가해주고, 필수값 처리와 문자열의 크기를 제한해줬다.
@PostMapping
public ResponseEntity<?> createEvent(@Valid @RequestBody EventRequestDto requestDto) {
try {
return new ResponseEntity<>(eventService.saveEvent(requestDto), HttpStatus.CREATED);
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("필수 입력 항목이 누락되었습니다.");
}
}
그리고 컨트롤러에서 유효성 검사를 해줬다.
`@Valid`는 단순 유효성 검증에 사용되며, 예외 발생 시 `MethodArgumentNotValidException`을 던진다.
`@Validated`는 그룹 검증을 추가로 지원하는데, 복잡한 유효성 로직이 필요한 경우에 유용하다.
@Column(nullable = false, length = 30)
private String title;
@Column(nullable = false, length = 200)
private String description;
지금 RequestDto, Entity, Controller 각각에서 유효성 검사를 계층별로 나눠서 해주는 이유는 코드의 책임을 분리하고 시스템의 유연성과 유지보수성을 높이기 위함이다.
| 위치 | 역할 | 유효성 검사의 목적 |
| 요청 DTO (Request DTO) | 클라이언트가 보낸 데이터를 캡슐화 | 형식과 값 검증 (예: 빈 문자열, 이메일 형식 등) |
| 컨트롤러 (Controller) | 요청과 응답 처리 | 전체적인 유효성 흐름 제어 (`@Valid`, 에러 핸들링 등) |
| 엔티티 (Entity) | DB 매핑 객체 | 데이터의 무결성 유지 (예: DB에서 null 허용 여부, 제약조건 등) |
이렇게 중복 검사를 하는 이유는 보안성을 향상하고, 책임을 분리하고 또 다양한 요청/응답 포맷에 대응하기 위함이다.
Entity에는 가능한 최소한의 제약만 넣고, 복잡한 검사는 DTO 또는 Service에서 수행하는 것이 일반적이라고 한다.
SPRING 숙련
객체 지향 설계 원칙: SOLID
SOLID는 객체지향 프로그래밍에서 유지보수성과 확장성을 높이기 위한 5가지 핵심 설계 원칙이다.
원칙설명예시 및 해결
| 원칙 | 설명 | 예시 및 해결 |
| SRP (단일 책임 원칙) | 클래스는 하나의 책임만 가져야 함 | User의 역할 분리 → AuthService, UserRepository 등으로 분리 |
| OCP (개방 폐쇄 원칙) | 확장엔 열려 있고, 수정엔 닫혀 있어야 함 | Shape 인터페이스 도입 → 새로운 도형 추가 시 기존 코드는 수정 필요 없음 |
| LSP (리스코프 치환 원칙) | 자식 클래스는 부모 클래스를 대체할 수 있어야 함 | ElectricCar를 Acceleratable 인터페이스로 분리해 문제 해결 |
| ISP (인터페이스 분리 원칙) | 불필요한 기능은 강제하지 않도록 인터페이스 분리 | Animal을 Runnable, Swimmable 등 작은 인터페이스로 나누기 |
| DIP (의존관계 역전 원칙) | 고수준 모듈은 저수준 구현에 의존하면 안 됨 | Notifier 인터페이스로 추상화 → EmailNotifier, SMSNotifier 등 유연한 확장 가능 |
SOLID의 핵심은 다형성이다. 하지만 다형성만으로는 OCP, DIP를 충분히 지킬 수 없기 때문에 추가적인 설계가 필요하다.
Spring Framework와 객체 지향
Spring은 위 원칙들을 실현할 수 있도록 도와주는 프레임워크로, IoC, DI를 통해 OCP와 DIP를 구현 가능하게 해준다.
SOLID 원칙을 지키며 개발하다 보면 개발자의 부담이 커짐으로 이를 효율적으로 처리하기 위해 SPRING이 등장했다.
현실적인 개발 환경에선 변경 가능성이 낮은 경우, 구현 클래스를 직접 사용하고 이후 변경 필요 시 인터페이스로 리팩토링하는 방식이 선호된다.
추상화는 유연성을 높이지만, 불필요한 경우엔 개발 비용만 증가하므로 상황에 맞는 적절한 판단이 중요하다.
Spring Container
Spring 애플리케이션에서 객체(Bean)를 생성, 관리, 소멸하는 역할 담당한다.
설정 파일이나 어노테이션을 읽어 Bean을 등록하고 의존성 주입(DI)을 수행한다.
Java: new 키워드로 직접 객체 생성
Spring: 컨테이너가 Bean으로 객체 생성하고 관리
장점: 객체 간 결합도 감소 (OCP/DIP), 인터페이스 기반 설계 가능, 의존성 자동 주입(DI) 지원
Container 종류
BeanFactory: 최상위 인터페이스, 기본 Bean 조회 및 관리
ApplicationContext: BeanFactroy의 확장판, 다양한 기능 제공 (실무에서 거의 사용)
Spring Bean
Spring Container가 관리하는 객체를 의미한다.
역할: 애플리케이션의 핵심 구성 요소, 다른 Bean과 의존 관계를 맺고 협력
특징: Spring이 생성하고 관리, 기본적으로 Singleton, DI로 의존성 설정 가능
생명주기: 생성 → 초기화 → 사용 → 소멸
| Bean 등록 방법 | 설명 |
| XML | 태그 사용 |
| Annotation | `@Component`, `@Controller`, `@Service`, `@Repository` |
| Java 설정파일 | `@Configuration` + `@Bean` 사용 |
Singleton 패턴
싱글톤(Singleton) 패턴은 클래스의 인스턴스를 오직 하나만 생성하도록 하는 디자인 패턴이다. 설정이나 데이터 관리 등 하나만 존재해야 하는 객체에 적합하며, 전체 애플리케이션에서 동일한 인스턴스를 공유해 사용한다. 메모리를 절약하고, 상태 일관성을 유지하며, 어디서든 객체에 접근할 수 있는 장점을 가진다.
| 구현 방식 | 설명 |
| Eager Initialization | 클래스 로딩 시 즉시 인스턴스를 생성. 간단하지만 유휴 자원이 될 수 있음 |
| Lazy Initialization | 실제 필요 시점에 인스턴스를 생성. 초기화 지연으로 메모리 효율↑ |
| Thread-safe Singleton | 멀티스레드 환경에서도 하나만 생성되도록 동기화 처리 (synchronized 등) |
| Double-Checked Locking | 성능 개선을 위한 최적화된 동기화 방식. 인스턴스 중복 생성을 방지 |
| Enum Singleton | 자바에서 권장되는 방식. 직렬화와 리플렉션 문제를 자연스럽게 회피 |
주의점: 싱글톤 패턴에서의 객체가 상태를 가지는(stateful, `this.value =` 같이 필드에 값을 저장) 경우 여러 클라이언트가 동시에 접근하면서 데이터 불일치나 동시성 문제가 발생할 수 있다. Spring에서는 이런 문제를 방지하기 위해 Bean을 반드시 무상태(stateless, 메서드 파라미터로 받은 값을 처리하고 반환만 함)로 설계해야 한다.
Bean 등록 방식
| 등록 방식 | 설명 | 장점 | 단점 |
| 자동 등록 `@ComponentScan` + `@Component` |
지정된 패키지에서 어노테이션 붙은 클래스를 자동으로 Bean으로 등록 | 코드 간결, 빠른 개발 | 제어 어려움, 충돌 가능성 |
| 수동 등록 `@Configuration` + `@Bean` |
직접 설정 클래스에서 Bean을 생성하고 등록 | 명확한 제어, 외부 클래스 등록 가능 | 코드 길어짐, 관리 복잡 |
| XML 등록 (구버전) | XML 파일에 Bean 정보를 명시 | 설정과 코드 분리 | 가독성 낮음, 유지보수 어려움 |
`@SpringBootApplication`은 `@ComponentScan`을 포함한다. Bean 이름은 클래스명 또는 메서드명을 기반으로 자동 생성된다.
ComponentScan 동작 흐름
1. `@ComponentScan`이 지정된 패키지를 탐색
2. `@Component`, `@Service`, `@Repository`, `@Controller` 등 어노테이션이 붙은 클래스 탐색
3. Spring 컨테이너에 Bean으로 등록
4. DI를 통해 다른 Bean과 연결
Bean 충돌
| 충돌 유형 | 결과 |
| 자동 vs 자동 | `ConflictingBeanDefinitionException` 발생 |
| 수동 vs 자동 | 수동 Bean이 우선권을 가져 자동 Bean을 오버라이딩 |
| 해결 방법 | Bean 이름 변경 또는 application.properties에서 설정 변경 spring.main.allow-bean-definition-overriding=true |
같은 타입의 Bean 충돌 해결
1. `@Autowired` + 필드명 → 타입으로 주입 후, 필드/파라미터명으로 매칭
2. `@Qualifier` → Bean에 구분자("...") 지정하여 명시적 주입 가능 (생성자, setter 모두 사용 가능)
3. `@Primary` → Bean에 우선 순위를 지정
자동 vs 수동 Bean 등록
| 구분 | 자동 등록 | 수동 등록 |
| 방식 | Annotation 기반 (`@Component` 등) | `@Bean` 또는 Java Config |
| 장점 | 편리함, OCP/DIP 준수, Spring Boot 기본 제공 | 외부 객체 등록 가능, 명시적 설정, 유지보수 유리 |
| 사용 시점 | 일반적인 개발 흐름에서 사용 | 외부 라이브러리, 기술 설정, Bean 선택 필요 시 |
의존관계 주입(DI)
| 주입 방식 | 특징 | 장점 | 단점 |
| 생성자 주입 | `final` 필드 + 생성자 | 불변성 보장, 테스트 용이, 컴파일 시점 오류 확인 | 코드 길어짐 (→ Lombok으로 해결) |
| Setter 주입 | `@Autowired` + setter 메서드 | 선택적 주입 가능 | 외부에서 수정 가능 → 불안정 |
| 필드 주입 | `@Autowired` 직접 필드에 | 코드 간결 | 테스트 어려움, 순수 Java 사용 불가 |
| 일반 메서드 주입 | `@Autowired` + 일반 메서드 | 유연한 주입 가능 | 거의 사용되지 않음 (대체 가능) |
Spring에서는 생성자 주입 방식이 가장 권장된다. 객체의 불변성을 보장하고, 필수 의존성이 누락될 경우 컴파일 단계에서 오류를 발견할 수 있어 안정성이 높다. 또한 테스트 코드 작성이 용이하고, 순환 참조 문제도 앱 실행 전 단계에서 확인 가능하므로 유지보수와 설계 측면에서 유리하다. Lombok 라이브러리의 `@RequiredArgsConstructor`로 간편하게 사용할 수 있다.
Validation
클라이언트가 보낸 요청 데이터가 사전에 정의된 조건에 부합하는지 확인하는 과정 (유효성 검증)
| 유형 | 설명 | 예시 |
| 프론트 검증 | 즉각적인 사용자 피드백 제공, 하지만 보안에 취약 | 비밀번호에 특수문자 포함 여부 알림 |
| 서버 검증 | API 명세에 따라 오류 응답, 사용자 사용성 개선 | 잘못된 폼 입력 시 상세 오류 메시지 제공 |
| DB 검증 | 데이터 무결성 유지 위한 최종 방어선 | Not Null, Default 설정 등 |
BindingResult: Spring에서 사용자의 입력 데이터 검증 시 발생하는 오류를 저장하는 객체이다. 오류가 있어도 서버가 예외를 던지지 않고, 컨트롤러가 정상 실행될 수 있게 도와준다.
마치며
오늘도 실습을 통해 Spring을 활용해보고 강의를 통해 좀 더 심화된 내용들을 들으면서 Spring이 정말 개발자의 부담을 줄여주는 프레임워크라는 걸 다시금 느꼈다. 또 각 layer들을 왜 나누고 책임과 역할이 잘 분배된 구조가 중요한지도 더 알게 되는 시간이었다. 더 명확하고 단순한 코드로도 원하는 걸 다 구현해내는 개발자가 되고 싶다.