Today's Codakata
// 카드 뭉치
class Solution {
public String solution(String[] cards1, String[] cards2, String[] goal) {
String answer = "Yes";
int i = 0, j = 0, k = 0;
while (i < goal.length) {
if (j < cards1.length && goal[i].equals(cards1[j])) {
j++;
i++;
} else if (k < cards2.length && goal[i].equals(cards2[k])) {
k++;
i++;
} else {
answer = "No";
break;
}
}
return answer;
}
}
public String solution(String[] cards1, String[] cards2, String[] goal) {
int i = 0, j = 0;
for (String word : goal) {
if (i < cards1.length && word.equals(cards1[i])) {
i++;
} else if (j < cards2.length && word.equals(cards2[j])) {
j++;
} else {
return "No";
}
}
return "Yes";
}
뭔가 아쉬움이 남아서 향상된 for문으로 리팩토링해봤다.
-- 특정 옵션이 포함된 자동차 리스트 구하기
SELECT CAR_ID, CAR_TYPE, DAILY_FEE, OPTIONS
FROM CAR_RENTAL_COMPANY_CAR
WHERE OPTIONS LIKE '%네비게이션%'
ORDER BY CAR_ID DESC
-- 조건에 부합하는 중고거래 상태 조회하기
SELECT BOARD_ID, WRITER_ID, TITLE, PRICE,
CASE WHEN STATUS = 'DONE' THEN '거래완료'
WHEN STATUS = 'SALE' THEN '판매중'
WHEN STATUS = 'RESERVED' THEN '예약중' END AS STATUS
FROM USED_GOODS_BOARD
WHERE CREATED_DATE = '2022-10-05'
ORDER BY BOARD_ID DESC
-- 취소되지 않은 진료 예약 조회하기
SELECT A.APNT_NO, P.PT_NAME, P.PT_NO, A.MCDP_CD, D.DR_NAME, A.APNT_YMD
FROM APPOINTMENT A
JOIN PATIENT P ON P.PT_NO = A.PT_NO
JOIN DOCTOR D ON D.DR_ID = A.MDDR_ID
WHERE A.MCDP_CD = 'CS'
AND DATE(A.APNT_YMD) = '2022-04-13'
AND A.APNT_CNCL_YMD IS NULL
ORDER BY APNT_YMD
테이블 3개도 조인이 가능함을 알게 되었다!
Today I Learned
Filter
공통 관심사 (Cross-Cutting Concerns)
공통 관심사란 여러 위치에서 반복적으로 사용되는 부가 기능을 의미합니다.
비즈니스 로직과 별도로 동작하는 부가 로직 (예: 인증, 로깅, 인코딩, 캐싱 등)
인증 처리 예시
로그인한 사용자만 API를 사용할 수 있도록 제한하는 방법
| 방법 | 문제점 |
| 화면에서 막기 | 클라이언트 요청 조작 가능 → 보안 취약 |
| Controller에서 체크 | 중복 코드 발생, 유지보수 어려움 |
| Spring AOP 사용 | 메서드 단위로 처리 가능하지만 HTTP 요청 전체를 다루기엔 한계 |
| Servlet Filter / Spring Interceptor 사용 | HTTP 요청을 전처리하여 인증 로직을 중앙 집중 처리 가능 |
Servlet Filter
공통 관심사를 중앙에서 처리
모든 요청/응답을 필터링
Filter Chain 구조로 여러 필터를 순차적으로 적용
핵심 메서드: `doFilter()`
동작 흐름: 클라이언트 요청 → Filter(로그인 실패 시 Filter에서 요청 차단) → Servlet → Controller
Filter 인터페이스 (jakarta.servlet.Filter)
`init()`: 필터 초기화 (서블릿 컨테이너 시작 시 1회 호출)
`doFilter()`: 요청마다 호출, `@Override`해서 필터 로직 구현
`destroy()`: 필터 종료 (서블릿 컨테이너 종료 시 1회 호출)
@Slf4j
public class CustomFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
log.info("Request URI = {}", requestURI);
chain.doFilter(request, response); // 다음 필터 또는 서블릿 호출
}
} // 예시 코드
Filter 등록 (Spring Boot)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean<CustomFilter> customFilter() {
FilterRegistrationBean<CustomFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CustomFilter());
registrationBean.addUrlPatterns("/*"); // 적용할 URL 패턴
registrationBean.setOrder(1); // 필터 실행 순서
return registrationBean;
}
}
`@WebFilter` 어노테이션도 가능하지만, 필터 순서 지정이 어려워 잘 사용하지 않습니다.
Filter 정리
| 항목 | 설명 |
| 구현 | Filter 인터페이스 구현 후 Bean으로 등록 |
| 요청 처리 | `doFilter()`에서 공통 로직 처리 후 `chain.doFilter()`로 다음 단계 호출 |
| URL 패턴 | `addUrlPatterns()`로 적용 범위 지정 |
| 순서 지정 | `setOrder()`로 필터 실행 순서 설정 |
| 다운캐스팅 | `ServletRequest` → `HttpServletRequest`로 변환하여 사용 |
JWT와 Spring Security
Spring Security는 JWT, OAuth 등 인증 방식을 표준화된 방식으로 지원
Servlet Filter로도 JWT 인증 구현 가능하지만,
Spring Security를 사용하면 보안 결함 위험 감소
인증/인가 로직을 더 안전하고 일관성 있게 관리 가능
2주차 강의 정리
1. Cookie
웹 브라우저에 저장되는 데이터로, 서버가 클라이언트의 상태를 기억할 수 있도록 도와준다.
로그인 상태 유지 등에 활용되며, 클라이언트 측에서 관리된다.
보안에 취약하므로 민감한 정보를 저장해서는 안 된다.
사용자가 임의로 수정할 수 있기 때문에 신뢰성이 낮다.
2. Session
서버에서 중요한 정보를 보관하며, 로그인 상태를 유지하는 방식이다.
클라이언트는 SessionId만 저장하고, 실제 정보는 서버에 존재한다.
SessionId가 탈취되더라도 민감한 정보는 서버에 있으므로 직접적인 피해는 제한적이다.
만료 시간을 설정하여 탈취 위험을 최소화할 수 있다.
HttpSession은 최근 요청 시간을 기준으로 만료 시간을 갱신하며, 일정 시간 동안 요청이 없으면 자동으로 세션이 종료된다.
3. Token
인증 및 인가 과정에서 사용되며, 사용자 또는 시스템의 신원과 권한을 증명하고 요청의 유효성을 검증하는 데 사용되는 디지털 문자열이다.
Session과 달리 클라이언트가 직접 Token 데이터를 저장한다.
서버는 상태를 저장하지 않는 Stateless 구조를 기반으로 하므로 확장성이 뛰어나다.
Cookie를 사용할 수 없는 모바일 환경에서도 활용 가능하다.
Token의 Payload는 암호화되지 않기 때문에 민감한 정보는 포함하지 않아야 한다.
만료 시간을 설정하여 탈취에 대비할 수 있다.
4. JWT (JSON Web Token)
인증에 필요한 정보를 JSON 형태로 담아 암호화한 Token이다.
Signature를 통해 위조 여부를 검증할 수 있어, 안전하게 관리할 수 있다.
JWT의 목적은 정보 보호가 아닌 위조 방지에 있다.
클라이언트가 Token을 보관하고, 서버는 이를 검증하여 인증/인가를 처리한다.
5. Filter
공통 관심사를 하나의 입구에서 처리할 수 있도록 도와주는 구조이다.
인증, 로깅, 인코딩 등 여러 요청에 반복적으로 적용되는 로직을 중앙 집중적으로 관리할 수 있다.
요청이 들어오기 전에 필터를 통해 전처리하거나, 응답을 반환하기 전에 후처리할 수 있다.
JPA
JPA란?
Java의 ORM 기술 표준으로, 객체와 관계형 DB 간의 패러다임 불일치 문제를 해결해준다.
JDBC를 직접 다루지 않고, 객체 중심으로 DB 작업 가능하다.
대표 구현체: Hibernate
※구현체(Implementation)란?
인터페이스나 추상 클래스에서 정의한 기능을 실제로 동작하게 만든 클래스
JPA에선 JPA가 정의한 기능을 실제로 동작하게 만드는 라이브러리들을 말한다.
JPA 사용 이유
1. 생산성: 객체 조작만으로 DB 작업 가능, SQL 작성 최소화
2. 유지보수성: 필드 변경 시 SQL 자동 처리, 코드 수정이 쉬움
3. 패러다임 불일치 해결: 객체 지향 구조를 그대로 DB에 반영 (상속, 연관관계 등)
4. 성능 최적화: 1차 캐시, 쓰기 지연, 지연/즉시 로딩으로 효율적인 DB 접근
Hibernate Dialect
DB마다 다른 SQL 문법을 자동으로 맞춰주는 설정이다. (예: MySQL은 `LIMIT`, Oracle은 `ROWNUM`)
JPA는 특정 DB에 종속되지 않으며, DB 교체가 자유롭다.
PersistenceContext
영속성 컨텍스트란?
JPA가 데이터베이스와 직접 소통하지 않고, 중간에 객체를 임시로 관리하는 메모리 공간이다.
이 공간을 통해 객체를 효율적으로 저장, 수정, 삭제, 조회할 수 있게 된다. (EntityManager를 통해 접근)
DB와의 중간 캐시 역할함으로 성능 향상, 일관성 유지, 자동 변경 감지, 쓰기 지연 등의 이점이 있다.
동작 흐름
1. 객체 생성 → 아직 아무것도 관리되지 않음 (비영속)
2. `em.persist(entity)` 호출 → 영속성 컨텍스트에 등록 (영속)
3. 객체 수정 → 컨텍스트가 변경 사항을 감지
4. 트랜잭션 커밋 → 변경된 내용을 DB에 반영 (쓰기 지연 + 변경 감지)
5. `em.remove(entity)` 호출 → 삭제 요청
6. `em.clear()` 또는 `em.close()` 호출 → 컨텍스트에서 분리 (준영속)
| 기능 | 설명 |
| 1차 캐시 | 동일한 객체를 DB 대신 메모리에서 조회 |
| 동일성 보장 | 같은 트랜잭션 내에서는 같은 객체 반환 |
| 변경 감지 | 객체 수정 시 자동으로 UPDATE SQL 생성 |
| 쓰기 지연 | SQL을 모아서 트랜잭션 커밋 시 실행 |
| flush | 변경 내용을 DB에 반영 (자동 또는 수동) |
※Tip
영속성 컨텍스트는 트랙잭션 범위 내에서만 유지된다.
`@Transactional`을 사용하면 자동으로 컨텍스트가 생성되고 관리된다.
컨텍스트가 사라지면 객체는 더 이상 관리되지 않으므로, 수정해도 DB에 반영되지 않는다.
JPA Entity 만들기
@Entity와 @Table
`@Entity`: 자바 클래스를 DB 테이블로 바꾸기 위해 사용하고 JPA가 해당 클래스를 관리 대상으로 인식한다.
`@Table(name = "테이블명")`: 실제 DB에 어떤 이름의 테이블로 만들지 지정해준다. (생략 시 클래스명 = 테이블명)
제약사항 - `final`,`enum`,`interface`,내부 클래스에는 사용할 수 없다. JPA가 객체를 수정, 생성하는데 이런 타입은 제약이 많다.
persistence.xml
JPA가 어떻게 동작할지 알려주는 설명서 같은 역할을 한다.
DB 연결 정보: 어떤 DB에 접속할지 (`url`,`user`,`password`)
Entity 등록: 어떤 클래스를 테이블로 만들지 (`<class>`)
DDL 자동 생성 설정: 테이블을 자동으로 만들지 여부 (`hibernate.hbm2ddl.auto`)
SQL 출력 설정: 콘솔에 SQL를 보여줄지 (`show_sql`,`format_sql`,`use_sql_comments`)
DDL 자동 생성 옵션
| 옵션 | 설명 | 추천 상황 |
| create | 기존 테이블을 지우고 새로 만듦 | 개발 초기 |
| create-drop | 실행 후 테이블 만들고, 종료 시 삭제 | 테스트용 |
| update | 변경된 부분만 반영 | 개발 중 (주의 필요) |
| validate | 매핑만 확인, 테이블은 건드리지 않음 | 실무 추천 |
| none | 아무것도 하지 않음 | 실무 추천 |
필드 매핑 어노테이션 (자바 필드를 DB 컬럼으로 연결)
| 어노테이션 | 설명 |
| @Column | 컬럼의 이름, 길이, null 허용 여부 등을 설정 |
| @Enumerated | enum 타입을 숫자 또는 문자열로 저장 |
| @Temporal | 날짜 타입을 어떤 형식으로 저장할지 지정 |
| @Transient | DB에 저장하지 않을 필드 (임시 계산용 등) |
| @Lob | 큰 텍스트나 바이너리 저장용 (예: 긴 글, 이미지 등) |
기본 키 설정
`@Id`: 이 필드가 기본 키(PK)라는 의미를 가진다. 직접 값을 넣는 방식이다.
`@GeneratedValue`: DB가 자동으로 키를 생성해주는 방식으로 전략을 선택할 수 있다.
| 전략 | 설명 |
| IDENTITY | MySQL 등에서 사용. DB가 자동 생성 |
| SEQUENCE | Oracle 등에서 사용. 시퀀스 객체 필요 |
| TABLE | 별도 테이블에서 키 관리 |
| AUTO | DB 종류에 따라 자동 선택 (주의 필요) |
제약 조건 설정
@Column(unique = true, nullable = false, length = 20)
`unique`: 중복 금지 / `nullable`: null 허용 안 함 / `length`: 최대 길이 20자
JPA 연관관계 매핑
객체와 객체 사이의 관계를 데이터베이스 테이블과 연결하는 방법이다. (예: 강사(Tutor)는 회사(Company)에 소속된다.)
단방향 연관관계
객체 A가 객체 B를 참조하지만, B는 A를 모르는 상태 (예 Tutor → Company는 알지만, Company → Tutor 모름)
단방향은 설정이 간단하고 유지보수가 쉽지만 반대 방향 탐색은 불가능하다.
@Column(name = "company_id") // 1
private Long companyId;
@ManyToOne // 2
@JoinColumn(name = "company_id")
private Company company;
1. FK만 사용하는 방식 (비객체 지향)
Tutor가 Company의 ID만 저장한다.
객체를 직접 참조하지 않기 때문에 `tutor.getCompany()` 같은 표현은 불가능하다.
2. 객체 참조 방식 (객체 지향)
Tutor가 Company 객체를 직접 참조한다.
`tutor.getCompany()`로 바로 접근 가능하다.
양방향 연관관계
객체 A가 객체 B를 알고, 객체 B도 객체 A를 아는 상태 (예: Tutor ↔ Company)
양방향을 설정하면 양쪽에서 자유롭게 탐색 가능하다. 하지만 관계를 잘못 설정하면 데이터 불일치나 성능 문제가 생길 수 있다.
// Tutor.java
@ManyToOne
@JoinColumn(name = "company_id")
private Company company;
// Company.java
@OneToMany(mappedBy = "company")
private List<Tutor> tutors = new ArrayList<>();
Tutor는 Company를 참조하고, Company는 Tutor 목록을 참조한다.
연관관계의 주인
양쪽이 서로 참조할 수 있는 객체와 달리 DB에서는 외래 키(FK)를 한 테이블에만 저장하기 때문에, JPA는 누가 FK를 관리할지 정해야 한다. 이 외래 키(FK)를 가지고 있는 쪽이 연관관계의 주인이 된다.
| 규칙 | 설명 |
| 주인은 `mappedBy`를 사용하지 않음 | FK를 직접 관리 |
| 주인이 아닌 쪽은 `mappedBy`를 사용 | 반대편 필드 이름을 지정 |
| 주인만 외래 키를 등록/수정 가능 | 반대편은 조회만 가능 |
언제 어떻게 써야 할까?
| 유형 | 특징 | 추천 상황 |
| 단방향 (ID 참조) | 단순, 객체 지향 아님 | 빠른 개발, 테스트 |
| 단방향 (객체 참조) | 객체 지향적, 탐색 제한 | 대부분의 실무 |
| 양방향 | 탐색 자유, 관리 복잡 | 양쪽 탐색이 필요한 경우 |
| 연관관계 주인 | FK 관리 책임 | 항상 FK가 있는 쪽으로 설정 |
Spring Data JPA
Spring Framework에서 JPA를 적은 코드로 더 쉽게 다양한 기능들을 사용할 수 있도록 도와주는 도구이다.
기존 JPA는 설정이 복잡하고 코드가 반복되기 쉬운데, Spring Data JPA는 이런 불편함을 줄여준다.
| 항목 | JPA | Spring Data JPA |
| 설정 | 직접 EntityManager, 트랜잭션 관리 | 자동 설정 (Spring Boot가 해줌) |
| 코드 | CRUD 직접 작성 | 인터페이스만 선언하면 자동 구현 |
| 생산성 | 낮음 | 매우 높음 |
| 확장성 | 유연함 | 편리함 + 확장 가능 |
EntityManager
JPA에서 DB와 소통하는 핵심 객체로 데이터를 저장하거나 조회할 때 사용한다.
원래는 직접 생성하고 닫아야 했지만, Spring에서는 자동으로 관리해준다.
Spring Boot에서는 `@PersistenceContext`로 주입받기만 하면 끝!
Repository
데이터를 저장하고 꺼내는 저장소이다.
Spring Data JPA에서는 JpaRepository라는 인터페이스를 상속하면 자동으로 기능이 구현된다.
Query Methods
메서드 이름만으로 SQL을 자동 생성하는 기능이다.
복잡한 쿼리는 @Query를 사용해서 직접 작성할 수도 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByNameOrderByModifiedAt(String name); // 수정된 시간 순으로 이름 조회
Member findByNameAndAddress(String name, String address); // 이름과 주소로 조회
}
→ 자동으로 다음 SQL이 만들어짐: `SELECT * FROM member WHERE name = ? AND address = ?;`
JPA Auditing
엔티티가 언제 생성되고 수정되었는지 자동으로 기록해주는 기능이다.
공통 필드를 BaseEntity라는 부모 클래스에 만들어두고, 다른 엔티티가 상속하면 자동으로 작동한다.
Spring Boot에서는 @EnableJpaAuditing을 설정해줘야 활성화된다.
마치며
오늘 하루 동안 배운 것들을 정리하면서 드는 생각은, 아직 모르는 게 너무너무 많지만 그래도 조금씩 성장하고 있다는 것이다. 처음엔 어려웠던 개념들도 직접 써볼수록 이해가 잘 됐고, 작은 리팩토링이어도 코드가 깔끔해질 때의 기쁨도 느낀다. 아직 서툴고 헤매기 부지기수지만, 이렇게 기록하고 정리하며 하루하루 성장하는 것이 뿌듯하다. 언젠가 이 기록들이 나만의 커리어로 이어지길 바라며 내일은 다음 프로젝트를 도전해보려한다!