동현의 나불나불
백엔드
배포
docker, AWS, github action을 활용한 배포
github_actions_기초
prometheus_이론
kafka 공부
ch1.kafka 시작하기
언어
자바
java 인터페이스
java다형성
java예외
java추상화
java컬렉션
javaThread
프로젝트
바로그림
django-학습-핵심-정리
elasticsearch-검색과-데이터-파이프라인-정리
활용한 기술
바로그림-프로젝트-설계와-시행착오
회고록
백엔드_통합점검
오늘의 식사
Today I Learned
2026_04_15~2026_04_19
2026_04_15
2026_04_16
2026_04_17
2026_04_20~2026_04_27
2026_04_20
2026_04_21
2026_04_22
2026_04_23
2026_04_24
2026_04_27~2026_05_01
2026_04_27
2026_04_28
2026_04_29
2026_04_30
2026_05_01
2026_05_04~2026_05_08
2026_05_04
2026_05_05
2026_05_06
2026_05_07
2026_05_08
2026_05_11~2026_05_15
2026_05_11
2026_05_12
2026_05_13
2026_05_14
2026_05_15
2026_05_21~2026_05_22
2026_05_21
2026_05_22
2026_05_27~2026_05_30
2026_05_27
2026_06_TIL
2026_06_11
2026_06_15
2026_06_16
2026_06_22
2026_06_25
2026_06_26
2026_06_28
2026_07_TIL
cs
네트워크
네트워킹 하향식 접근
http 프로토콜
데이터베이스
DB_SQL기초
Lock_diffrent
웹
os
chapter1 운영체제 서론
chapter2 운영체제 구조
chapter3 프로세스 관리
Home
Contact
Copyright © 2024 |
Yankos
Home
>
Today I Learned
> 2026_05_11~2026_05_15
Now Loading ...
2026_05_11~2026_05_15
2026-05-15 일지
Spring Security HTML 폼태그 방식 HTML 폼을 통해 사용자 ID와 비밀번호를 입력받아 서버에서 검증하는 방법. 기본 설정 유저네임인증필터 사용자 로그인 요청 /login 으로 POST 요청 UsernamePasswordAuthenticationFilter가 요청 가로챔 이 필터는 Spring Security에서 폼 로그인 인증을 처리하는 필터 username, password 추출 AuthenticationManager에게 인증 요청 UserDetailsService 등을 통해 사용자 정보 조회 비밀번호 검증 인증 성공 시 SecurityContext에 인증 정보 저장 실패 시 로그인 실패 처리 정보 보안 3요소(CIA) Confidentiality(기밀성) : 정보를 볼 수 있는 사람만 볼 수 있어야 한다. 잠그는 것을 의미 Integrity(무결성) : 정보가 함부로 바뀌지 않고, 정확하게 유지해야 한다. 무단수정 x Availability(가용성) : 필요할 때 정보를 사용할 수 있는 상태. 정보는 적합한 인증 절차를 통과했으면 원할 때, 바로 사용 할 수 있어야 한다. XSS, 크로스사이트 스크립트 사이트에 악성 자바스크립트를 숨겨 놓는것, 이를 통해 우리를 운영하는 페이지를 통해 고객들을 공격하는 것이다. 하지만 브라우저는 이에 대한 검사를 해주지는 못한다. 여기서 자바스크립트는 어떤 스크롤을 누르고 있는지, 키보드, 마우스의 위치에 대해 캡쳐링이 가능하다. 이러한 데이터를 다른 곳에 통신을 통해 보내는 것이 가능하여 조심해야 한다. CSRF Cross Site Request Forgery Forgery는 위조라는 의미로 , 사용자의 공격을 다른 사이트로 보내는 것을 의미한다. 위조된 사이트를 이용해서 진짜로 인증받은 다른 정보를 편취(갈취)해서 요청을 보내서 정보를 갈취하는 행동 위조 지폐랑 비슷한 느낌, 사용자가 이게 위조되었는지 잘 모른다. 개발자가 똑같은 사이트로 클론코딩하는 것이 가능하다. 위조된 사이트에서 googe처럼 피싱을 유도하는 기능을 수행한다. 이거를 어떻게 막을 수 있을까? CSRF 토큰이 있다. CSRF 토큰은 html 페이지를 내려줄 때 키를 쌍으로 발급한다. 이 키는 반대쪽 키가 있어야 돌아가는 키이다. 한쪽은 내가 세션에다가 묶어두고 하나는 내가 내려준 응답 어딘가에 비밀스럽게 넣어준다. 인증이 들어왔을때는 이 두가지 키를 이용하여 검증을 수행하는 것 내가 내려준 응답에 대해서만 보안을 적용하겠다는 의미이다. 이러한 csrf 토큰을 사용하지 않는다면 spring security를 적용한 웹을 사용한다면 403에러가 뜨게 되어있다. CORS Cross-Origin Resource Sharing CORS에러는 웹 개발시 나타날 수 있는 에러 종류 중 하나이다. 동일 출처 정책(same-origin policy)를 어긴 경우 중 하나인데 동일 출처 정책은 xss, csrf와 같은 공격을 방지하기 위해 동일한 출처에서만 리소스를 공유할 수 있다라는 법률이다. 배포할때는 이를 조심해야 한다. 다 똑같이 적용해야 한다. thymeleaf 이에 관한 대책이 세워져 있지만 다른 프론트엔드 언어를 사용하여 구현할 때는 이를 조심해야 한다. → 이 문제는 브라우저에서 잡는 것이다. 출처가 의미하는 것이 무엇인가. 프로토콜, 도메인, 포트가 출처를 의미한다. 스키마가 프로토콜이다. 도메인과 포트까지 본다. 다른 프로토콜, 도메인, 포트에 대해 읽을 수 있어야 한다. 우리사이트에 있는 정보를 이용해도 되는가? 허용된 친구들만 우리서버의 자원들을 가지고 갈 수 있게 묶어두는 것이 필요하다. 하지만 개발을 하다보면 어쩔수 없이 다른 출처간의 상호작용을 해야하는 케이스가 존재한다. 이와 같은 예외상황에서 적용가능한 것이 CORS 정책이다. SOP정책을 위반해도 CORS정책에 따르면 다른 출처의 리소스도 허용이 된다. 웹브라우저가 보안상 다른 도메인의 서버 자원에 접근하는 것을 제한하는 SOP를 우회하여 안전하게 자원을 공유하도록 허용하는 HTTP 헤더 기반 메커니즘, 다른 출처의 자원 공유에 대한 정책이다. 브라우저의 CORS 기본동작 클라이언트에서 http 요청의 헤더에 Origin을 담아 전달 서버는 응답헤더에 Access-Controll-Allow-Origin을 담아 클라이언트로 전달 이 리소스에 접근하는 것이 허용된 출처 url을 내려보낸다. 클라이언트에서 Origin과 서버가 보내준 Access-Controll-Allow-Origin을 비교한다. 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교한후 차단할지 말지를 결정 서버에서 Access-Controll-Allow-Origin 헤더에 허용할 출처를 기재해서 클라이언트에 응답을 하면 되는 것 암호화 중요한 내용을 아무나 알아보지 못하도록 바꾸어 놓는 것 크게 단방향, 양방향으로 종류가 나뉘게 된다. 단방향은 한번 암호화를 하면 원래로 못돌아간다. 일방통행이다. 암호화는 될 수 있지만 복호화는 될 수가 없다. 이를 통해 나오는 어떠한 값을 해시 값이라고 부른다. 암호화 된 값을 원본으로 되돌린다면 이것을 구분할 수 있을까? → 단방향 암호화를 하면 항상 똑같은 해시값이 나오기 때문에 이 해시값과 비교를 해서 값이 옳은지를 확인을 한다. 양방향은 암호화 한 내용을 다시 복호화 할 수 있는 것이다. 모르는 기호를 다시 복호화를 하여 복구를 하는 것이다. 단방향과 양방향의 차이는 원래값으로 되돌릴 수가 있는가? 우리는 단방향으로 로그인을 구현한다. 실무에서는 비밀번호는 암호화 되서 들어가게 된다. 예를 들어 중국의 “모르면 괜찮아 안걸리면 돼” 걸리기 전까지는 내는 것은 없지만 걸리면 2배이상으로 는 경우가 있다. 이렇게 행동하는 것을 방지하기 위해 Spring Security에는 나오는 비번이 평문이면 해킹당했다고 취급하고 로그인을 거부하게 된다. 우리 업계에서 표준처럼 사용하는 암호화 알고리즘 Bcrypt, 크립토 툴 → 암호화 툴, 단방향 암호화 방식이다. 정확히는 비밀번호 해싱함수이다. Bcrypt의 원리는 이 암호화 해시안에서 되게 긴 문자열을 만들고 섞게 된다. 아이디 별로 섞게 된다. 이렇게 만들어 놓는 것을 Salt라고 한다. 양념친다, 소금친다 이러한 느낌으로 사용된다고 한다. Bcrypt는 암호화를 할때마다 다른 salt가 나오도록 했다. 스프링 시큐리티는 이러한 Bcrypt 방식을 차용해서 사용한다 Bcrypt Bcrypt : 업계 표준 암호화 알고리즘으로, 비밀번호 해시 방식을 사용하며 단방향 암호화이다. 솔트(Salt) 생성하고, 비밀번호와 솔트를 결합한 뒤, Blowfish 암호화 알고리즘을 기반으로 바이트를 수천 번 섞고 연산(Round)하는 방식이다. Salt : 단방향 함수의 추가 입력으로 사용되는 랜덤 데이터 문자열 암호화 할때마다 솔트값을 다르게 하여서, 동일한 값이어도 암호화 할 때마다 다르게 저장된다. 해시 형태 : $[버전]$[cost]$결과 OAuth2.0 방식 권한을 위임하는 프로토콜. 신용 있는 서비스들에게 인증의 책임을 위임을 한다. 이런 서비스들이 개인정보를 보관하고, 책임을 지게된다. Resource Owner 리소스 소유자. 우리의 서비스를 이용하면서, 구글, 페이스북 등의 플랫폼에서 리소스를 소유하고 있는 사용자이다. 리소스라 하면 ‘구글 캘린더 정보’, ‘페이스북 친구 목록’, ‘네이버 블로그 포스트 작성’ 등이 해당된다. Authorization & Resource Server Authorization Server는 Resource Owner를 인증하고, Client에게 액세스 토큰을 발급해주는 서버이다. Resource Server는 구글, 페이스북, 트위터와 같이 리소스를 가지고 있는 서버를 말한다. 인증 요청 : 고객이 인증 요청 권한부여 승인코드 요청 : 구글에 개인정보 요청. 개인정보를 얻을 수 있는 토큰을 요청 로그인 페이지 : 고객이 구글 로그인 인증 (구글이 인증 해줌) 권한부여 승인코드 전달 AccessToken 요청 : 권한부여 승인코드로 구글 인증 서버에 AccessToken을 요청 AccessToken 전달 : 인증서버가 권한부여 승인코드가 유효한지 검토하고 AccessToken을 전달 자원 요청 : AccessToken으로 Resource Server에 개인정보 요구 자원 전달 : Resource Server가 AccessToken 검증후 그에 맞는 개인 정보 전달 인증완료 Yaml 파일 설정하면 Spring Security가 1~8번을 해줌 인증 완료 후에는 서버-구글 간의 AccessToken 만료기간까지 유지. 개인 정보 접근 범위는 우리가 설정 한 것 및 고객의 동의를 의미한다. AccessToken은 매우 민감하고 중요한 코드이므로, 보관시 무조건 양방향 암호화 해서 보관하거나, 폐기 JWT (Json Web Token) 식별하기 위하여 웹에서 사용하는 문자열 키. 디코딩 하면 JSON 형태이다 \한 번 만들면 철회 불가 하며, 민감한 정보를 빠르게 만들 수 있다는 장점이 있다. OAuth2.0을 한다고 해서 JWT 토큰을 필수적으로 사용 할 필요는 없음. 구조 : 헤더.페이로드.시그니쳐, jwt는 검은색 .을 기준으로 3가지로 분류된다 헤더 : 서명을 암호화한 알고리즘의 이름 페이로드 : 토큰의 정보인 본문. 페이로드는 아무나 다 내부를 볼 수 있다. 따라서 민김한 정보를 페이로드에 넣으면 안 된다. 시그니쳐 : 암호화된 서명. (단방향 암호화를 하고 압축함) JWT 토큰은 문자열이다. 한번 만들면 철회가 불가능하다.문자열은 한번 만들면 철회가 안된다. jwt는 유효기간이 있는데 한번 노출되면 유효기간이 끝날 때까지 유효하다. 이것을 보완상충하기 위한 전략이 있다. access token과 Refresh token으로 분리하여 access token을 짧은 수명을 가진 것으로 활용하고 새로운 Access Token을 재발급하는데 Refresh token을 사용하게 된다. http 특성으로는 비연결성과 비상태성이 있다. 이 중 비상태성을 가지는데, 똑같은 것이 보낸 것은 어떻게 기억하고 있을까? 쿠키하나를 세팅해준다. localhost를 가면 JSESSIONID가 있다. 이는 spring boot + spring security에서의 기본 세션 쿠키 이름이다. 여기서 이것을 없애면 세션을 찾을 수 없게 되고 다시 로그인을 해줘야 한다. 브라우저에서는 jsessionID라는 세션으로 쿠키로 기억하고 있다. 스프링 세큐리티가 생각보다 많은 것을 해줄수 있게 된다. 스프링 시큐리티의 원리는 필터가 들어간다. 필터로 이루어진 필터 체인이 있다.
Today I Learned
· 2026-05-15
2026-05-14 일지
QueryDSL DSL (Domain-Specific Language)은 특정 분야의 문제 해결에 특화된 프로그래밍 언어라고 볼 수가 있다. 그리고 DSL의 종류 중에 External DSL과 Internal DSL이 있는데 QueryDSL은 이 중 기존 프로그래밍 언어 안에서 만들어지는 Internal DSL이고 Java코드로 SQL쿼리를 작성하기 위한 DSL이다. QueryDSL을 통해 동적으로 JPQL을 작성 할 수 있게 한다. 의존성 3가지(QueryDSL-core, APA, JPT Support)를 추가하여 사용해주자 // 핵심 라이브러리 (실행 시 사용) // Source: https://mvnrepository.com/artifact/io.github.openfeign.querydsl/querydsl-jpa implementation 'io.github.openfeign.querydsl:querydsl-jpa:7.1' // Source: https://mvnrepository.com/artifact/io.github.openfeign.querydsl/querydsl-core implementation 'io.github.openfeign.querydsl:querydsl-core:7.1' // QClass 생성을 위한 도구 (빌드 시 사용) // Source: https://mvnrepository.com/artifact/io.github.openfeign.querydsl/querydsl-apt annotationProcessor 'io.github.openfeign.querydsl:querydsl-apt:7.1:jakarta' // 어노테이션 해석을 위한 API annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" 💡 QueryDSL은 현재 orderby부근에서 SQLInjection 관련 취약점이 존재하나 원래 QueryDSL쪽에서 개선이 없다. 이를 fork 해서 openfeign 쪽에서 개선하고 개발하고 있는 코드를 제공하고 있기 때문에 maven repository 안에서 openfeign쪽의 QueryDSL을 찾을 필요가 있다. // QueryDsl 설정 @Configuration public class QueryDslConfig { @Bean public JPAQueryFactory queryFactory(EntityManager entityManager){ return new JPAQueryFactory(entityManager); } } QueryDSL을 사용하기 위해 JPAQueryFactory를 스프링 빈으로 등록한다. private final JPAQueryFactory queryFactory; @Transactional(readOnly = true) public Optional<Item> findByCode(String code){ return Optional.ofNullable(queryFactory.selectFrom(item) .where(item.code.eq(code)) .fetchFirst()); } queryFactory를 통해 손쉽게 빌더 패턴을 사용하여 jpql을 구현하여 데이터를 넘겨줄 수 있게 된다. CQRS 패턴 (명령 쿼리 책임 분리) 읽기랑 쓰기 모델 DB를 분리하는 것 데이터의 변경과 조회를 다른 모델로 처리하는 패턴을 의미한다. 장점 : 독립적인 크기 조정 가능, 각 목적에 맞는 최적화된 데이터 스키마, 관심사의 분리 , 더 간단한 쿼리 단점 : 애플리케이션 디자인 복합성 증가, 최종 일관성 문제 Spring DB Migration DB의 변경사항을 다 기록하여 관리해주는 것 Flyway : sql로 작성하여 관리가 쉬움, DB간 호환 힘듦 Liquibase : yaml로 작성하여, 복잡함. DB간 호환이 좋다. 데이터베이스를 만들어주면 안에 건드리지 않은 테이블이 나오게 되는데 이는 flyway_schema_history이다. 이것으로 마이그레이션 툴을 시험해본다. flyway_schema_history는 어떤 SQL마이그레이션이 이미 실행되었는지 기록하는 일을 수행한다. 문제를 보고 다시 실행시켜준다. 이때 이미 실행된 v1,v2와 같은 migration은 다시 실행하지 않는다. spring batch도 위와 유사한 기능을 갖고있다. spring batch를 붙이면 위와 같은 스키마가 만들어지게 된다. applcation.yml 파일에 필요한 설정을 한다. Migration 파일 명명 규칙 파일 이름을 읽어서 실행 순서를 정하기 때문에 이름이 매우 중요하다 형태 : __.sql [Prefix] V : Versioned Migration 한 번만 실행 됨. 버전 순서대로 R : Repeatable Migration checksum 변경되면 재실행 version 기재 X U : Undo 잘 안쓰임 [Version] 숫자 . _ 사용 가능 V1_1 V1_2 V2 V3 등. 더 작은 것부터 실행 보통 _를 많이 씀 [Description] Github message 같은 느낌. 설명 공백 대신 _ 사용 마이그레이션을 통한 버전관리를 V3__add_post_status.sql처럼 기능을 추가시키는 것이 가능하다. spring 생태계 위는 Monolitic 구조로 spring 생태계를 표현한 것이다. 단순히 spring Data JPA, spring WebMVC 말고도 알아야 하는 것이 매우 많음을 알 수가 있다. 하지만 이렇게 Monolitic 구조로 서비스를 운영하게 되면 단 하나의 장애로 인해 다른 모든 기능을 이용할 수 없게 된다. 이와 같은 문제를 보완하기 위해 나온 구조가 MSA(Micro Service Architecture)이다. 이는 여러 작은 구조를 합쳐서 하나의 서비스로 만드는 것이다. 여기에는 입력을 한 통로로 받아야 하는 것이 있는데 이를 spring gateway이고 클라우드로 구현한 것이 spring cloud이다. 또한 spring이 쿠버네티스와 호환이 좋아져서 spring k8s를 사용할 줄 알아야 한다. 이처럼 단순 mvc구조와, jpa를 사용할 줄 아는 것만으로는 규모의 서비스를 제공하는 것은 힘들다. Spring Security 첫걸음 웹에서 규모있는 서비스를 제공하기 위해서는 필요한 보안조치를 할 필요가 있다. 스프링에서 어떠한 방식으로 sequrity를 제공하고 있을까? 모든 해킹을 완전히 막는 것은 불가능하다. 아무리 견고한 방어를 만들더라도 미세한 틈을 뚫고 들어온다. 완벽했다고 생각하는 방어벽도 뚫리고 다시 막는 것의 반복이다. 그렇지만 우리는 적절한 보안 조치로 해킹의 성공 확률을 많이 낮출 수가 있다. 또한 백엔드에서도 어느정도 보안에 관한 조치를 취할 필요가 있다. csrf, xss 와 같은 잘알려진 공격은 우리가 막아주는 것이 좋다. 우리가 취할 수 있는 최대한의 조치는 해줘야 한다. 스프링 시큐리티는 집단지성이라고 봐도 된다. 백엔드에서 마주친 여러가지 보안 대책이 모아진 spring 하위 프레임워크 중 하나이다. 인증, 인가 등을 활용한 애플리케이션의 리소스 접근 제한등 가장 기본적이고 필요한 보안 기능을 제공해준다. 추가적으로 이 spring security는 보안에 관한 내용을 커스터마이징 해준다. jwt를 사용한 통신방식, OAuth 2.0 사용등을 사용하게 된다. Interceptor은 controller에 요청이 들어가기전에 부가적인 작업을 수행할 수 있도록 도와주는 객체이다. WAS에서 Dispatcher Servlet 내부로 들어오지 않은 것은 논리적으로 위임하기 전에 catch를 한것은 위임하지 않았다고 봐도 된다. dispatcher servlet에 들어가기 전에 이시점에서 interceptor해서 무언가를 판별하고 수행하는 것을 Filter라고 한다 필터라는 친구는 Dispatcher servlet에 들어가기 전에 수행하는 것을 의미한다. 정수기 내부에는 여러가지 정수를 거치기 위해 필터가 존재한다. 각각의 필터는 내가 거를 수 있는 불순물만 거스른다. 내가 못거르는 것은 다음 필터를 통해 거르도록 한다. Spring Security에서 사용하는 filter도 이와 동일하다. filter chain이라는 것을 사용하여 자신이 여과할 수 있는 것만 거쳐서 여과되도록 한다. 다음 필터로 이동해서 깨끗한 물이 나오도록 하는 방식이다. dispatcher servlet을 거쳐서 나오게 한다. 이러한 필터를 통해 스프링 어플리케이션에 대해 보안과 접근이 가능하다. 이게 스프링 시큐리티에 관한 전반적인 내용이다. 다음은 무엇을 봐야 할까? 인증과 인가를 봐야 한다. 회사에는 여러가지 기밀이 존재한다. 회사는 여러가지 이유로 회사의 관계자가 아닌 이상 회사의 구역에 접근하는데 제한을 두게 된다. 내가 회사의 관계자라는 것을 증명해서 회사에 입장을 할 수 있게 된다. 인증 Authentication 본인이 누구인지 인증하는 것이 인증, Authentication이다. 인증이 되어야지 스스로에 대한 정보를 밝혀서 들어갈 수 있게 된다. 계정과 비밀번호를 통해 밝히는 나라는 정보를 밝히는 것이다. 이러한 서비스에 접근하기 위해서는 인증이 필요하다. 요즘 많이 사용하는 인증으로는 소위 sns로그인이라고 불리는 방식이 OAuth 2.0 방식이 있다. 이것은 추후에 다루도록 하겠다. 인가 Authorization 인증은 되었으나 우리가 들어갈 수 있는 건물이 다를 수가 있다. 이처럼 인증된 회원을 구분할 수 있게 하는 것이 인가이다. 인가는 전제가 뭐냐면 인증이 완료된 사람한테 어떠한 권한이 있는 지 확인하는 것을 의미한다. 로그인은 인증이라면 회원 등급이 인가라고 볼 수가 있다. 인증이라는 것은 인가의 대전제라고 봐도 무방하다. Spring Security 에서 인증을 어떻게 수행하고 있을까? Filter을 수단으로 사용한다. 우리가 따로 여러가지를 정의하지 않더라도 이 Spring Security를 통해 적용된다. 인증정보를 보게된다. 우리 관계자라면 요청을 처리할 수 있도록 다음 곳으로 넘겨주고 필터를 통해 막고 다음으로 되돌려 보낸다. 이게 스프링 시큐리티에서 인증을 위한 사용방식이다. 그 중 하나로 계정과 비밀 번호를 통해 보안을 수행하는 방식이 존재한다. 이 스프링 시큐리티를 주입받으면 어떠한 url을 적용해도 보이게 된다. 사용자 이름과 비밀번호를 찍어준다. 이 계정을 유저네임과 패스워드에 입력을 해주고 사용자 인증을 할 수 있게 된 것이고 다른 페이지에 접근을 할 수 있게 된다. 당연히 html 페이지가 어떻게 구성되어 있는가를 보면 이 페이지는 html form 태그로 이루어져 있다. 여기에는 두가지 input 태그가 있다. 유저네임, 패스워드가 포함된다. sign in 을 눌러 들어가게 되면 username은 뭐고 패스워드가 뭔지 나오게 된다. 이 방식에서 인증을 위한 방식으로 username, password를 통한 폼태그를 사용한다. 이게 기본적인 흐름이다. 이러한 방식을 form based authentication이다. 별도의 설정을 하지 않는 이상 폼기반 동작방식으로 진행이 이뤄진다.
Today I Learned
· 2026-05-14
2026-05-13 일지
Batch Process 일괄 처리 방식 Batch는 일정량의 작업을 모아서 한번에 처리하는 방식이다. 대량의 쿼리를 실시간으로 계속 처리한다면 서버 부하 증가, 응답 속도 저하와 같은 부작용이 있기때문에 모아서 작업을 할 필요성이 생긴다고 할 수가 있다. 벌크 쿼리 단순히 하나, 두개의 쿼리가 아닌 여러 개의 쿼리 public List<Item> saveAll(List<Item> items){ //batch final int BATCH_SIZE = 50; for (int i = 0; i< items.size(); i++){ entityManager.persist(items.get(i)); if(i % BATCH_SIZE ==0 && i>0){ entityManager.flush(); entityManager.clear(); log.info("FLUSH :)"); } } entityManager.flush(); entityManager.clear(); log.info("FLUSH :)"); return items; } 배치 크기로 flush를 반영을 하고 java에는 생성자는 있지만 소멸자는 없기 때문에 객체 정보를 삭제해주기 위해서 clear() 메서드를 사용해 영속성 컨텍스트를 비워준다. 예를 들어 만약 275개의 데이터가 있고 현 배치크기는 50이기 때문에 반복문만 사용한다면 나머지 25개의 데이터에 대해 처리를 해주지 않는다. 이 때문에 나머지 데이터에 대해 처리를 해주기 위해 밖에서 한번더 flush()와 clear()를 해주게 된다. @Test void it_will_save_all(){ int SIZE = 1_000; List<Item> items = generateItemList(SIZE); List<Item> savedList = repository.saveAll(items); assertThat(savedList.size()).isEqualTo(SIZE); savedList.forEach( item -> assertThat(item.getId()).isNotNull() ); List<Item> items1 = repository.findAll(); log.info("items1.size()={}", items1.size()); } Reflection jpa에서는 persist, remove, update같은 작업을 하게되는데 이 작업이 끝나면 java객체가 사라지는 것이 아니다. DB의 row가 사리지는 것이지 java객체는 여전히 jvm에 살아있다. 그리고 java에는 reflection이라는 기술이 있는데 이는 런타임 시점에 클래스 정보를 동적으로 분석하는 기술이다. findByNameAndPrice() 이런 메서드가 있으면 Reflection으로 find find → SELECT, delete → DELETE, save → Insert By Name And Price 메서드 명을 사용하여 사용자에 맞게 동적으로 쿼리를 구현하는 것이 가능하다. 이는 프록시가 메서드 호출을 감지하고 reflection을 통해 메서드 이름을 분석하고 그에 맞는 JPQL을 생성하여 Hibernate에 넘겨주는 것과 같다. 이러한 기능을 Query Method라고 한다. 조건이 복잡해지면 QueryDSL을 사용하게 된다. Spring Data JPA 앞에서 언급한 방식으로 repository를 구현할 수도 있지만 Spring Data JPA에서 지원해주는 인터페이스가 있다. JpaRepository<T, ID> T : 엔티티 클래스의 타입 ID : T의 식별자가 될 PK의 타입 CrudRepository에서 우리가 사용해온 saveAll, delete, findById 등의 CRUD 기능을 제공해준다. PagingAndSortingRepository에서는 페이징 기능을 제공해준다. 페이징의 장점 : oom(Out of Memory) 을 막고 가독성과 속도를 개선 한다 페이징이 필요한 이유 단순히 화면 가독성 기능 뿐만 아니라 성능 최적화 기술을 제공해준다. 만약 데이터가 100만 건의 대용량인 경우 전부 가져오면 메모리 사용량 증가, 속도 저하등 문제가 생길 우려가 있다. 하지만 페이징을 통해 다음 페이지를 지연조회, 부분조회를 하여 성능저하를 줄일 수가 있다. @DataJpaTest 만약 Repository만 검증하고 싶은데 Repository의 기능을 테스트하기 위해서는 컨트롤러, 서비스,보안 설정, 웹 설정 등을 띄우게 되면 테스트의 속도가 느려지고 불필요한 bean을 올리게 되어 테스트의 목적이 흐려진다. 이와 같은 경우를 생각해 spring boot에서 제공해주는 테스트 어노테이션이 있는데 대표적인 예시가 @DataJpaTest이다. 이 어노테이션을 사용하면 Repository Bean, EntityManager, Hibernate ,JPA Auditing 관련 구성, DataSource, 등 데이터 접근 계층에 필요한 Bean만 로딩된다. Controller,Service,Security, Web MVC 설정,ViewResolver 같은 Bean들은 기본적으로 제외된다. 즉, DB 접근과 관련된 최소한의 스프링 컨텍스트만 실행된다. 그렇게 되어 테스트 속도를 향상시키고 메모리 절약, 테스트 목적 명확화 등의 장점을 갖게된다. 참고 단축키 : shift + shift 원하는 클래스로 이동하는 것이 가능하다 디버깅 기존의 일반 hibernate, mybatis와 다르게 spring data JPA는 너무 추상화가 잘되어있어 사용하기는 편리하지만 버그가 생겼을경우 어떤곳에서 문제가 발생했는지 체크하기가 힘들어진다. 여기서는 breakpoint를 활용할 수가 있다. 그리고 breakpoint를 걸고 디버그 모드로 실행하면 프로그램은 해당 줄에서 멈추게 된다. step 기능 사용하기 step over : 현재 줄을 실행하고 다음 줄로 이동 step into : 현재 줄의 메서드 내부로 이동 step out : 현재 들어와 있는 메서드를 빠져나간다 resume : 다음 breakpoint까지 실행한다. 디버그와 로그는 함께 사용하는 것이 좋다. 그리고 Controller → Service → Repository 호출부 → Entity 값 → SQL 로그의 순으로 확인을 하면 좋을 것 같다. Spring Boot Mybatis와 그냥 MyBatis의 차이 MyBatis에서 개발자가 직접 xml파일에서 DataSource설정, SQLSessionFactory, Mapper등록 등 여러가지 설정을 수동으로 했지만 Spring Boot Mybatis에서는 Dependency만 추가하면 Spring Boot가 내부적으로 DataSource 생성, SqlSessionFactory 생성, Mapper 스캔, 트랜잭션 연결, Connection Pool 설정 등을 자동 처리해준다. 필요한 사항은 application.yml이나 application.properties에 적은 DB정보를 기반으로 자동으로 DataSource Bean을 생성하게 된다. DSL(Domain Specific Language) 특정 도메인을 대상으로 만들어진 특수언어, 특정 비지니스 도메인을 인터페이스로 만든 API라고 봐도 무방하다. Stream, Collector 등도 DSL이라고 할 수가 있다. 대표적으로 SQL, html이 존재한다. QueryDSL의 경우 Java코드로 쿼리를 작성하기 위한 DSL이라고 볼 수가 있는데 이는 SQL과 비슷한 의미를 Java 코드로 구현한다. DSL은 크게 External DSL, Internal DSL이 존재한다. External DSL은 독립적인 문법을 가진 DSL이다. 대표적인 예시는 SQL이다. 도메인을 매우 자연스럽게 표현할 수 있는 장점을 가지고 단점으로는 별도의 문법을 배워야 한다. Internal DSL은 기존 프로그래밍 언어 안에서 만들어지는 DSL이다. 예시로는 QueryDSL, Gradle이 있다. QueryDSL는 Java 안에서 사용하는 Internal DSL이고 gradle은 groovy,kotlin기반 Internal DSL이다.
Today I Learned
· 2026-05-13
2026-05-12 일지
테스트를 위한 환경구축 서론 이후 과정에서 배울 CI/CD 과정을 거치면 문제가 있는 코드를 메인 브랜치에 들어가지 못하게 하는 것이다. 또한 슬라이스 테스트를 하게 되면 DB가 필요한데 테스트는 가상의 컴퓨터에서 실행한다고 가정하기 때문에 DB가 없으면 무조건 실패를 하게 된다. 자바에서는 간단한 테스트, 메모리에서 실행, 설치 필요없음과 같은 이유로 H2를 테스트시 사용하는 데이터베이스로 많이 사용하게 된다. 스프링 부트 프로젝트에서는 운영용으로 MySQL, PostgreSQL등을 사용한다고 하면 테스트용으로 따로 H2를 설정해서 사용하는 것이 가능하다. 또한 테스트는 언제, 어디서 실행해도 같은 결과가 나와야 하기 때문에 운영DB에서 테스트를 실행하는 것보다는 테스트용DB, Mock객체등을 활용하여 구현하는 것이 조금 더 바람직한 방향이 될 것이다. auto-configuration Spring Boot가 개발자가 Bean을 하나하나 등록하지 않아도 프로젝트에 추가된 의존성과 설정파일을 보고 필요한 설정을 자동으로 구성해주는 기능이다. Spring boot에서는 외부에서 주입되는 application.yml을 통해 DB, 서버 포트, JPA 옵션과 같은 설정을 관리할 수가 있다. 이처럼 외부에서 필요한 설정을 가져오는 것을 Externalized Configuration이라고 한다. Spring Boot에서는 여러 설정소스를 정해진 우선순위에 따라 읽고, 더 높은 우선순위의 설정이 낮은 우선순위의 설정을 덮어쓴다. hibernate ddl-auto 설정 Entity 클래스와 DB 스키마를 비교해서, 애플리케이션 실행시 DDL을 어떻게 처리할 지 정하는 옵션이다. jpa: hibernate: ddl-auto: create #update # create-drop #create #validate validate 애플리케이션 실행 시 Entity 클래스와 DB 테이블 구조가 맞는지 검사만 한다. Entity와 DB 스키마가 일치하는지 확인만 하고, DB 구조는 절대 변경하지 않는다. 운영 환경에서 사용할 수 있는 비교적 안전한 옵션이라고 볼 수가 있다. update Entity 클래스와 DB 스키마를 비교해서 부족한 테이블이나 컬럼을 자동으로 추가/수정하려고 시도한다. 우리 entity 클래스에 맞추는 쪽으로 구현된다. 예를 들어 Entity에 새로운 필드를 추가하면 DB에 컬럼이 추가될 수 있다. 다만 다음과 같은 단점이 존재한다. 기존 컬럼을 지우지 않고 새 컬럼을 추가하면 default 설정을 해주지 않으면 불필요한 null 컬럼이 생길 수 있다. 이때문에 데이터 마이그레이션을 세밀하게 제어하기 어렵다 따라서 로컬에서는 사용가능하나 개발서버 이상에서 사용하기는 힘들다 create 애플리케이션이 시작될 때 기존의 테이블을 삭제하고 다시 생성한다. 즉 실행할 때마다 DB 구조가 초기화된다. create-drop create와 비슷하지만 애플리케이션 종료시점에 테이블을 다시 삭제한다. none Hibernate가 자동으로 DDL을 처리하지 않게한다. 정리 개발서버에는 어디까지 허용될까? create-drop, create를 사용하면 입력한 데이터가 날아가기 때문에 사용하면 안된다고 볼 수가 있다. 다만 개발서버와 로컬은 다르다. 개발서버는 내부에 배포가 되어있기 때문에 update가 적당한 것 같다. 다만 update를 하면 null 컬럼이 많이 생기므로 기본값을 넣은 상태로 진행하던가 서비스를 적용하기전에 스키마를 보고 적용하는 것을 하게 되는데 업데이트에서 보수적으로 진행하게 된다. 운영은 none이 적당하고 개발 서버는 update까지 적당하면 로컬에서는 다 해도 괜찮다고 볼 수 있다. 주의 spring boot starter 사이트에서 다운받아 압축을 풀어서 spring boot를 시작하는 경우 파일에 iso로 인코딩이 들어가서 yml 파일에 한글이 깨지는 경우가 있다. entity 설정 저번 hibernate와 JPA를 배웠을 때와 유사하게 전개를 하게 된다. 먼저 필요한 접근설정, id에 관해 설정을 한뒤 필요한 attribute를 넣어주게 된다. equals와 HashCode()사용 (동등성 검사) 자바에서는 기본적으로 메모리 주소를 비교하여 객체를 비교를 하게 되는데 동등성 검사를 위한 equals() 두 객체가 동일한 의미를 갖는지 여부를 나타낸다. 두 객체가 동등하다는 것은 그들의 상태가 동일하다는 것을 의미한다. equal()를 사용하여 같은 DB데이터를 같은 객체로 판단하기 위해 사용한다. java에서는 equals가 true이면 hashCode()로 나오는 해쉬값도 같아야 하기 때문에 같이 쓰는 것을 원칙으로 한다. 그리고 hashCode()에는 매개변수가 오면 안된다. Entity 저장전에 id가 없고 hashCode()가 변하면 collection이 깨질 수가 있기 때문이다. 외래키를 위한 별도의 attribute 설정 Pk인 id가 아닌 code라는 별도의 attribute를 통해 이번 실습에서 DB를 구현했다. Order id code 1 ORD-20260512-001 OrderItem id order_code 10 ORD-20260512-001 이렇게 구현할 때의 장점 비지니스 식별자가 명확하다 코드를 보면 대략적인 정보를 알 수가 있다. 외부 시스템 연동이 쉬워진다. 시스템간 PK 충돌 가능성이 감소하고 로그 추적이 쉬위지는 장점이 존재한다. Pk 전략 변경과 분리가능 내부 DB의 전략이 바뀌더라도 외부에 보여지는 것은 code이기 때문에 관리하기 쉽다. 이는 외부에서 쉽게 ID를 유추하기 힘들게 하는 장점이 존재한다. 그리고 DDD(도메인 주도 설계)에 가까워졌다고 볼 수가 있다. 단점 FK성능이 PK보다 느릴 수가 있다. 문자열로 구성되어서 인덱스 비용 증가, join 비용 증가등 이슈가 존재한다. code의 변경이 위험해진다. 연관된 FK에 전부 영향이 가고 데이터 무결성이 위험해진다. cascadeType.all을 @ManyToOne에서 하는 경우 위험해 질 수도 있다. 연결된 다른 테이블의 row까지 삭제할 위험이 존재한다. cascade를 사용하는 경우 OneToMany일 때 두도록 하자. TDD 실습 통합테스트 여러 객체가 실제로 함께 동작하는지 검증하는 테스트 여러 계층을 실제로 연결해서 테스트를 한다. @SpringBootTest 실제 Spring Container을 띄운다는 의미 Bean등록, 의존성 주입, AOP, JPA, 트랜잭션 등이 실제처럼 돌아가게 된다. 빌더패턴의 필요성 @Test void it_will_return_item_including_pk(){ final String NAME = "상품1"; final String CODE = UUID.randomUUID().toString(); final Integer PRICE = 10_000; Item item = new Item(CODE, NAME, PRICE); } 위 코드는 읽어보면 아무런 위험이 느껴지지 않지만 의미적으로 속성값이 잘못 들어가게 된다. 이는 생성자 방식의 한계로 필드가 많아지면 위험해진다. 하지만 빌더 패턴을 만들면 생성자 인자 순서를 안전하게 만들고, 객체 생성을 명확하게 할 수가 있다. Item item = Item.builder() .code(CODE) .name(NAME) .price(PRICE) .build(); 장점 의미가 명확해진다(테스트 코드에서 큰 장점을 가진다.) 순서 실수를 방지할 수가 있다 필드가 많아져도 안전하다 선택적 필드 처리가 가능하다 체이닝, 각 메서드가 자기자신을 return한다. 계속 이어붙이는 것이 가능하다. AssertJ java 테스트 코드에서 사용하는 Assertion(검증) 라이브러리이다. 보통 JUnit이랑 함께 사용한다. 기본적인 검증 메서드들 isEqualTo isGreaterThan isNotNull isTrue assertThat(findItem.getPrice()).isEqualTo(TARGET_PRICE); 일치하면 테스트 성공이 반환 되고 만약 일치하지 않는다면 테스트 실패가 반환된다. assertThatThrownBy() 예외를 발생시키는 검증, 이 코드를 실행했을 때 특정 예외가 발생하는지 검증하는 AssertJ함수 assertThatThrownBy(() ->repository.updatePrice(INVALID_CODE, TARGET_PRICE)) .isInstanceOf(NullPointerException.class); 람다 식과 함께 활용을 하게 되는데 뒤의 isInstanceOf()는 발생해야 할 오류 종류를 넣어주게 된다. 테스트에서는 실패 상황에 관해 검증을 할 필요가 있기 때문에 사용한다. 테스트 생명주기 어노테이션 보통 테스트 준비, 초기화, 정리 작업에 사용 BeforeAll : 모든 테스트가 실행하기 전 BeforeEach : 각각의 테스트가 실행하기 전 AfterAll : 모든 테스트가 실행한 후 AfterEach : 각각의 테스트가 실행한 후 BeforeAll, AfterAll은 객체가 생성되기 이전에 실행되고 static에만 사용할 수 있다 테스트 커버리지 내 코드가 테스트에 의해 얼마나 실행되었는가를 수치로 나타낸 것 Line Coverage 코드 한 줄이 실행되었는가 Branch Coverage if/else 모두 테스트 되었는지 비지니스 로직의 핵심부분을 구현한 서비스 계층에서 측정하는 것이 좋을 것 같다. 참고 값을 가져올때 단순한 하나의 값이 아닌 여러 개의 값을 가져와야 하는 경우가 있다. 이 객경우 대량의 쿼리를 날리는데 이때 bulk 쿼리를 사용해서 가져오도록 한다. bulk 쿼리는 배치작업으로 수행해서 전부 다 완료되면 commit을 하고 만약 하나라도 실패하면 rollback을 시켜 안정성을 확보하는 것도 하나의 방법이 될 수가 있다. 참고 관리자 모듈을 만드는 것도 연습해봐야 한다. 웹을 구현할 때 관리자 모듈을 통해 통계를 해주는 것이 골치아프다. 관리자 모듈을 만들 때 어느페이지에서 유저별로 특정 사항에 관한 조회, 통계, 집계 쿼리를 짜야한다. 그리고 각 부분에 대해 어디서부터 잘못되었는지에 대해 인지를 할 필요가 있다. 그리고 각 요구사항에 대해 쿼리를 짜는것이 도움이 된다.
Today I Learned
· 2026-05-12
2026-05-11 일지
AOP 핵심 비지니스 로직과 공통 기능(횡단 관심사)을 코드 레벨에서 분리하여 관리하는 프로그래밍 방식 주 관심사(Core Concern) 핵심 비지니스 로직 (프로그램의 본연의 핵심 기능) 횡단 관심사(Cross Cutting Concern) 여러 곳에서 반복되는 부가적인 기능 ex: 로그, 보안, 트랜잭션, 캐싱 TDD (Test Driven Development) 테스트 주도 개발 방법론. 개발 방향을 테스트를 먼저 만들고 개발 하는 것. TDD 회의론도 등장 (테스트 때문에 개발이 밀려 주객전도가 일어남) 단위 테스트 비지니스 로직 중의 가장 작은 핵심 비지니스 단위(메서)를 테스트 하는 것 Bean을 띄우지 않음(컨테이너 미사용) → Mock 사용 슬라이스 테스트 특정 계층 또는 특정 관심 영역만 잘라서 테스트하는 방식 관련된 Bean만 띄움 종류 @WebMvcTest Controller 계층 테스트에 사용한다. 웹상의 요청과 응답에 관한 테스트 URL 매핑, HTTP Method, Request Parameter, Request Body Response Status, View 이름, JSON 응답, Validation을 다루게 된다. 시큐리티, 필터까지 자동 포함 @WebMvcTest(PostController.class) class PostControllerTest { @Autowired MockMvc mockMvc; @MockBean PostService postService; @Test void 게시글_목록을_조회한다() throws Exception { mockMvc.perform(get("/posts")) .andExpect(status().isOk()); } } 여기서 중요한 점은 Service는 실제 객체가 아니라 보통 @MockBean으로 대체한다는 것이다. Controller O Service Mock Repository X DB X @DataJpaTest Repository와 JPA 테스트에 사용한다. @Transactional 어노테이션을 포함해 테스트 완료시 자동으로 롤백 Entity 매핑, Repository 메서드, JPQL, 쿼리 메서드, 연관관계 매핑 저장/조회/삭제을 검증하는데 사용한다. @DataJpaTest class PostRepositoryTest { @Autowired PostRepository postRepository; @Test void 게시글을_저장한다() { Post post = new Post("제목", "내용"); Post savedPost = postRepository.save(post); assertThat(savedPost.getId()).isNotNull(); } } 구성은 보통 이렇다. Controller X, Service X, Repository O, Entity O JPA O, DB O 보통 테스트용 내장 DB인 H2를 사용해서 테스트를 한다. @JsonTest JSON 직렬화/역직렬화 테스트에 사용한다. 예를 들어 DTO가 JSON으로 잘 변환되는지 확인한다. @JsonTest class PostRequestJsonTest { @Autowired JacksonTester<PostRequest> json; @Test void JSON을_DTO로_변환한다() throws Exception { String content = """ { "title": "제목", "content": "내용" } """; PostRequest request = json.parseObject(content); assertThat(request.getTitle()).isEqualTo("제목"); } } 통합 테스트 애플리케이션을 통합하여 테스트하는 방식 단위테스트는 어떤 bean도 안띄운다. 따라서 어떠한 spring container도 없이 그 기능만을 테스트하겠다는 것을 의미한다. 통합테스트는 모든 bean을 띄워서 실행하게 된다. 전부 다 띄운다는 의미는 redis, kafka, elk등의 툴도 띄우고 상호작용하는 것도 포함한다. 프록시 패턴 실제 객체 대신 대리 객체인 프록시가 먼저 요청을 받아 처리하는 패턴 코드예시 @Slf4j public class Proxy implements Subject{ private Subject targetObject; public Proxy(Subject targetObject) { this.targetObject = targetObject; } @Override public void operation() { log.info("[prefix] Proxy.operation()"); targetObject.operation(); log.info("[postfix] Proxy.operation()"); } } 횡단관심사와 프록시를 적용한 코드 예시 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Logging { } @Target: 이 어노테이션을 어디 붙일 수 있을지 설명 ElementType.TYPE : 클래스에 붙일 수 있다는 것을 의미 Element.METHOD : 메서드에 붙일 수 있다는 것을 의미 @Retension(RetentionPolicy.RUNTIME) : 어노테이션의 정보를 언제까지 유지할 것인지 지정, 생존 범위라고 봐도 무방 Retension 종류 종류 설명 SOURCE 컴파일 후 제거 CLASS 클래스 파일까지만 저장 RUNTIME 실행 중에도 유지 @Slf4j @Service @RequiredArgsConstructor public class NonAopService { private final AopRepository repository; public void logic1(){ log.info("[NonAopService] 서비스 로직 시작"); repository.logic(); log.info("[NonAopService] 서비스 로직 종료"); } @Logging public void specialLogic(){ log.info("[NonAopService] 스페셜 로직 시작"); repository.logic(); log.info("[NonAopService] 스페셜 로직 종료"); } } 커스텀한 인터페이스 Logging을 특정 메서드가 실행될 때 달아놓아서 보호 프록시 클라이언트의 권한에 따라 실제 객체의 메서드 호출을 제한 특정 클라이언트만 서비스 객체를 사용 할 수 있도록 하는 경우 가상 프록시 원본 객체가 생성이 너무 무거워서 오래 걸릴 때, 실제 객체가 생성될때까지 생성을 지연시키는 프록시 패턴 생성이 오래 걸리는 것은 첫번째만 오래 걸리고, 두번째는 캐싱을 해서 바로 준다 필요한 시점에는 대신 생성을 해준다 JDK 동적 프록시 (JDK Dymamic Proxy) 리플랙션 기술을 이용하여 동적으로 만들어 주는 프록시. 반드시 인터페이스가 있어야 한다. InvocationHandler라는 인터페이스를 상속받아서 사용한다. invoke는 요청하다라는 의미를 가지는 단어로 call과는 다르지만 간접적인 호출이라는 의미가 있다. @Slf4j @RequiredArgsConstructor public class PaymentHandler implements InvocationHandler { private final PaymentService paymentService; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { log.info("결제 전에 로깅을 수행하겠습니다"); Object result = method.invoke(paymentService, args); log.info("결제 후에 로깅을 수행하겠습니다"); return result; } } CGLIB (Code Generation Library) 인터페이스가 없는 클래스에 대하여 동적으로 적용해준다. 바이트코드 조작(java 소스코드를 직접 작성하지 않고, Runtime중에 클래스파일의 내용을 동적으로 생성하거나 수정하는 것)으로 프록시 생성한다. 기존 클래스를 상속받아서 동적으로 프록시 객체를 생성하는 방식으로 동작 주의 구현체가 final이면 안됨 부모 클래스의 생성자를 체크해야 한다. 기본 생성자가 필요하다 @Slf4j @RequiredArgsConstructor public class SubLogicInterceptor implements MethodInterceptor { private final Object target; @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { log.info("전처리"); Object result = proxy.invoke(target, args); log.info("후처리"); return result; } } MethodInterceptor을 스프링이 제공받아서 이를 사용해서 구현하는 것도 가능하다.
Today I Learned
· 2026-05-11
<
>
Touch background to close