Today's Codekata
// 2016년
class Solution {
public String solution(int a, int b) {
int[] month = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
String[] dayName = {"FRI","SAT","SUN","MON","TUE","WED","THU"};
int days = 0;
for (int i = 0; i < a - 1; i++) {
days += month[i];
}
days += b - 1;
return dayName[days % 7];
}
}
처음엔 LocalDate에 있는 기능을 사용해서 답을 찾았는데, 알고리즘은 편리한 방법보다 어떻게하면 그 값을 구할 수 있는지에 대해 좀 더 근본적인 방법을 생각해보는 시간이라고 느껴서 배열을 활용해서 답을 찾아봤다.
-- 과일로 만든 아이스크림 고르기
SELECT FH.FLAVOR
FROM FIRST_HALF FH
INNER JOIN ICECREAM_INFO I ON FH.FLAVOR = I.FLAVOR
WHERE FH.TOTAL_ORDER > 3000
AND I.INGREDIENT_TYPE = 'fruit_based'
ORDER BY FH.TOTAL_ORDER DESC
-- 재구매가 일어난 상품과 회원 리스트 구하기
SELECT USER_ID, PRODUCT_ID
FROM ONLINE_SALE
GROUP BY 1, 2
HAVING COUNT(*) > 1
ORDER BY USER_ID, PRODUCT_ID DESC
-- 최댓값 구하기
SELECT MAX(DATETIME)
FROM ANIMAL_INS
-- 조건에 맞는 사용자 정보 조회하기
SELECT U.USER_ID, U.NICKNAME,
CONCAT(U.CITY, ' ', U.STREET_ADDRESS1, ' ', U.STREET_ADDRESS2) AS "전체주소",
CONCAT(
SUBSTR(U.TLNO, 1, 3), '-',
SUBSTR(U.TLNO, 4, 4), '-',
SUBSTR(U.TLNO, 8, 4)
) AS "전화번호"
FROM USED_GOODS_BOARD B
INNER JOIN USED_GOODS_USER U ON B.WRITER_ID = U.USER_ID
GROUP BY U.USER_ID
HAVING COUNT(B.WRITER_ID) >= 3
ORDER BY U.USER_ID DESC
`CONCAT()`함수는 SQL에서 여러 문자열을 하나로 붙일 때 사용하는 문자열 연결 함수이다.
Today I Learned
Spring
Bean Validation
객체의 필드나 메서드에 제약 조건을 어노테이션으로 설정하여 유효성 검사를 수행하는 방식이다.
대부분의 프로젝트에서 사용되며 Spring Boot에서도 기본으로 통합됩니다. (대표 구현체: `Hibernate Validator`)
BindingResult를 함께 사용하면 오류 발생 시에도 컨트롤러가 호출되어 에러 메시지 처리가 가능하다.
| 주요 어노테이션 | 설명 |
| `@NotNull` | null 불가 |
| `@NotBlank` | null, "", " " 모두 불가 |
| `@NotEmpty` | null, "" 불가 |
| `@Range(min, max)` | 숫자 범위 제한 (Hibernate 전용) |
| `@Size(min, max)` | 문자열 길이 제한 |
| `@Email` | 이메일 형식 검증 |
| `@Pattern(regexp)` | 정규식 검증 |
Object Error 처리
필드 간 관계 검증 시 사용한다.
예시: price * count ≥ 10000 조건을 직접 Java 코드로 구현하여 `bindingResult.reject()`로 처리
groups 기능
상황별로 다른 검증 조건 적용할 수 있도록 도와주는 기능이다.
예시: 등록 시 `@Range` 적용, 수정 시 미적용
DTO 분리
검증 로직이 점점 복잡해지는 경우 상황별로 DTO를 분리하는 것이 유지보수 더 효율적이다.
등록용, 수정용 DTO를 따로 만들어 그룹이나 검증 어노테이션을 각각 설정해주면 좋다.
groups 기능 vs DTO 분리
| 항목 | groups 기능 | DTO 분리 |
| 목적 | 한 DTO에서 다양한 상황별 검증 조건 적용 | 상황별로 명확한 역할 분리 |
| 작성 난이도 | 설정은 간단하지만, 조건이 많아지면 가독성↓ | 각 상황에 맞춘 DTO로 가독성↑, 관리 용이 |
| 유지보수성 | 그룹 추가/변경 시 검증 로직 수정 필요 | 새로운 상황 발생 시 DTO 추가로 확장 가능 |
| 추천 상황 | 검증 조건이 단순하고 필드 구조가 동일할 때 | 조건이 복잡하거나 필드가 상황마다 다를 때 |
| 결합도 | 한 DTO에 로직이 모여 높은 결합도 | 책임을 분리해 낮은 결합도 유지 |
@ModelAttribute 와 @RequestBody
| 구분 | `@ModelAttribute` | `@RequestBody` |
| 바인딩 방식 | 필드 단위 | 객체 단위 |
| 바인딩 실패 시 | 컨트롤러 호출됨 | 컨트롤러 호출 안됨 |
| Validation 적용 | 바인딩 성공한 필드만 | 전체 객체 변환 성공 시 |
1주차 강의 정리
Spring Container: 객체 생성 및 관리, 의존성 주입 등을 담당
Spring Bean: Container가 관리하는 Java 객체
IOC(제어의 역전): 객체 생성 및 의존성 관리를 개발자가 아닌 Spring이 제어
DI: 객체 간의 의존성을 Spring이 주입해줌
Singleton Pattern: Spring Bean은 기본적으로 싱글톤으로 관리되어 하나의 인스턴스만 생성(상태를 가지지 않도록 설계)
Bean 자동 등록 방식(권장): `@ComponentScan(Spring Boot)` + `@Component`(`@Controller`, `@Service`, `@Repository`)
Bean 수동 등록 방식: `@Configuration` + `@Bean`
의존관계 주입 방식: `@Autowired` + 생성자 이용 (불변성 유지, 실수 방지)
Lombok 사용 시: `@RequiredArgsConstructor`로 final 필드 자동 주입
BindingResult: 검증 오류 정보를 담는 객체
Bean Validation: `@NotNull`, `@Size`, `@Range`등 어노테이션 기반 검증 기술
Cookie
1. HTTP는 상태를 기억하지 못하는 프로토콜이다
HTTP는 Stateless라서 클라이언트의 이전 요청 정보를 서버가 기억하지 못한다.
로그인 같은 사용자 상태를 유지하려면 쿠키가 필요하다.
2. 쿠키 헤더의 역할
`Set-Cookie`: 서버가 쿠키를 생성해 브라우저에게 전달하는 응답 헤더
`Cookie`: 브라우저가 저장된 쿠키를 서버에 함께 전송하는 요청 헤더
모든 요청마다 쿠키가 함께 전송되며, 네트워크 트래픽이 증가할 수 있으므로 쿠키 내용은 꼭 최소화해야 한다.
3. 쿠키의 생명주기
세션 쿠키: 브라우저 종료 시 삭제됨 (`expires`, `max-age` 미지정)
영속 쿠키: 설정된 기간까지 유지됨 (예: `max-age=3600` → 1시간 후 삭제)
`max-age=0` 또는 음수 → 즉시 삭제됨
4. 쿠키의 적용 범위 설정
`domain`: 특정 도메인이나 서브도메인에서만 쿠키 적용
`path`: 특정 경로에서만 쿠키 유효 (예: `/api` 설정 → `/api/example`만 유효)
불필요한 트래픽 방지를 위해 필요한 위치에만 적용되도록 제한해야 한다.
5. 쿠키의 보안 설정
`Secure`: HTTPS에서만 전송되도록 제한 (HTTP는 보안상 취약함)
`HttpOnly`: JavaScript로 쿠키 접근 불가능 → XSS 공격 방지
`SameSite`: 요청 도메인 불일치 시 전송 제한 → CSRF 공격 방지
`HttpOnly`와 `SameSite`는 실제 운영환경에서 반드시 적용해야 하는 옵션이다.
6. 인증 수단으로서의 쿠키 활용
쿠키는 인증에 사용될 수 있지만, 정보 노출과 조작 가능성 때문에 보안상 취약하다.
민감 정보를 직접 저장해서는 안 되며, Session 방식이나 JWT와 병행하는 방식이 권장된다.
로그인 인증 흐름에서의 쿠키 처리
1. 로그인 성공 시 서버는 user 식별 정보를 `Set-Cookie`로 브라우저에 전달한다
→ 예: `Set-Cookie: userId=1`
2. 브라우저는 이후 모든 요청에 자동으로 `Cookie` 헤더를 포함하여 서버에 전달한다
→ 예: `Cookie: userId=1`
3. 세션 쿠키를 사용하면 브라우저 종료 시 자동 로그아웃 처리된다
→ 설정값 없이 생성하면 세션 쿠키가 기본
4. 서버에서는 `@CookieValue`를 통해 쿠키에서 `userId`를 꺼내고,
DB에서 사용자 조회 후 유효한 경우 화면에 출력할 데이터를 Model에 담아 View에 전달한다
쿠키 기반 인증의 취약점
값 탈취 가능: 네트워크 구간에서 쿠키 값이 노출될 수 있으며, 탈취 시 재사용 가능
값 조작 가능: 클라이언트가 쿠키를 수정하면 다른 사용자로 인식될 수 있다
실무에서의 보안 대처 방법
| 항목 | 설명 |
| 민감 정보 금지 | 주민번호, 사용자 식별번호 등 직접적인 개인정보는 저장하지 않음 |
| 난독화된 식별자 사용 | 일반인이 이해할 수 없는 토큰 형태로 서버와 매핑하여 사용자 식별 |
| 짧은 유효시간 설정 | 토큰 만료 시간을 짧게 설정하여 탈취 시 피해 최소화 |
| 강제 만료 처리 | 의심되는 경우 토큰을 무효화 (예: 접속기기/IP가 다른 경우 등) |
보안을 강화하려면 쿠키 단독 사용보다는 JWT 또는 서버 세션 방식으로 인증 처리하는 것이 바람직하다.
Session
1. Session 개념
Session: 서버가 사용자 정보를 저장하며 로그인 상태를 유지하는 방식
클라이언트는 `SessionId`를 `Cookie`로 서버에 전달
서버는 Session 저장소를 통해 사용자 정보를 조회
2. Session 생성 및 동작 흐름
① 로그인 성공 → 서버가 임의의 `SessionId` 생성 (UUID)
② 서버가 `SessionId`와 사용자 정보를 Session 저장소에 저장
③ 클라이언트는 `SessionId`를 `Cookie`로 저장하고 모든 요청에 포함
④ 서버는 받은 `SessionId`로 사용자 정보를 꺼내 활용
3. HttpSession 사용
`request.getSession(true)` → Session이 없으면 새로 생성
`request.getSession(false)` → Session이 없으면 `null`
`session.setAttribute(key, value)` → 데이터 저장
`session.invalidate()` → Session 삭제
4. `@SessionAttribute` 활용
Spring에서 제공하는 어노테이션으로 Session에 저장된 값을 꺼낼 수 있음
Session을 새로 만들지는 않음 (`required = false`로 설정 시 선택적 사용 가능)
5. Session 정보 조회
`session.getId()` → 세션 고유 식별값
`getMaxInactiveInterval()` → 유지시간(기본 30분)
`getLastAccessedTime()` → 마지막 접근 시간
`session.isNew()` → 새로 생성된 세션인지 여부
6. Session 타임아웃 및 문제점
기본적으로 30분 후 자동 삭제
브라우저 종료와는 무관하게 서버는 알 수 없음
많은 유저가 로그인할 경우 서버 메모리 과부하 발생 가능
Session은 상태를 유지하므로 확장성에 제약이 있음
Cookie vs Session 비교
| 항목 | Cookie | Session |
| 저장 위치 | 클라이언트 (브라우저) | 서버 |
| 용도 | 로그인 상태, 방문 기록 등 | 로그인 정보, 사용자 상태 유지 |
| 데이터 노출 | 클라이언트에서 확인 가능 | 서버에만 존재 |
| 수명 | 설정 가능 / 브라우저 종료 시 삭제 | 자동 만료 (기본 30분) |
| 확장성 | 가볍고 클라이언트 의존적 | 서버 부담 증가, 확장에 제약 있음 |
※ 인증 vs 인가
인증 (Authentication): 사용자가 누구인지 확인 (예시: 로그인)
인가 (Authorization): 사용자가 어떤 권한을 갖는지 확인 (예시: 회원 전용 페이지 접근, 수정 권한 등)
Token과 JWT
1. Token이란?
인증(Authentication)과 인가(Authorization)을 위한 디지털 문자열
사용자 또는 시스템의 신원 확인 및 요청 검증에 사용됨
서버가 아닌 클라이언트에 저장 → 서버 부담 감소
2. Token의 특징
| 항목 | 설명 |
| 저장 위치 | 클라이언트 (브라우저, 앱 등) |
| 인증 확장성 | 다양한 플랫폼에서 사용 가능 |
| 구조 | 보통 사용자 정보를 담고 있고, 서버는 Token 유효성만 검사함 |
| 단점 | 용량이 큼, 민감 정보 암호화되지 않음, 탈취 시 대처 어려움 |
3. JWT (JSON Web Token) 구조
Header: 토큰 타입 및 해시 알고리즘 정의
Payload: 사용자 정보와 인증 관련 클레임 포함
Signature: Header와 Payload를 Secret Key로 암호화한 서명
Base64 URL 방식으로 인코딩됨 (쉽게 디코딩이 가능함으로 중요 데이터 포함 X)
서명은 서버가 검증 → 위·변조 여부 확인
4. JWT 인증 흐름
① 클라이언트가 로그인 요청
② 서버가 사용자 정보로 JWT 생성 (서명 포함)
③ JWT를 Authorization 헤더에 포함해 서버 요청
④ 서버는 서명을 기준으로 유효성 검사 후 요청 처리
5. JWT 보안 특징
위조 방지: 서명을 통해 변조 여부 검증 가능
민감정보 제한: Payload는 암호화되지 않음 → 비밀번호 등은 포함 금지
탈취 위험: Client 저장 방식 → 탈취 시 재사용 가능성 존재
6. JWT 장단점
| 장점 | 단점 |
| 서버 인증 저장소 불필요 | Payload 비암호화 |
| 수평 확장에 유리 | 토큰 길이 증가로 트래픽 부담 |
| 다양한 클라이언트 인증 적용 가능 | 탈취 시 대응 어려움 |
7. Access Token & Refresh Token
Access Token: 인증 후 API 요청에 사용, 유효 시간 짧음
Refresh Token: Access Token 만료 시 재발급 요청에 사용, 보통 DB에 저장
보안성과 UX 균형 위해 두 종류를 함께 사용
Refresh Token으로 자동 로그인 및 토큰 갱신 가능
마치며
오늘 공부하면서 코드를 어떻게 쓰느냐도 중요하지만, 누가 봐도 이해하기 쉬운 구조로 정리해서 짜는 게 정말 중요하다는 걸 다시 한 번 느꼈다. 단순히 기능만 구현하는 게 아니라, 향후 유지보수까지 생각하며 고민하는 습관이 필요하다는 생각을 끊임없이 하게 되는 것 같다. 개념들을 단순히 외우는 게 아니라 왜 그런 구조가 나왔는지, 어떤 선택이 더 유지보수에 좋은지를 고민해보는 시간이 되었다.