- 블로그를 날려먹었다가 복구해서 이미지가 깨져있습니다 ㅠㅠ
(사내 위키에 썼었던 글을 옮깁니다.)
실험 결과로서 타임아웃을 줄이는 것이 메모리와 관계가 있다는 것은 알았지만 왜 그런지 궁금하여 조사해 본 결과를 정리해 보았습니다. (원래 스프링 시큐리티의 구조를 간단히 설명하는 글을 저도 공부하면서 써볼까 했는데 시간이 오래 걸려 미루었습니다...)
스프링 시큐리티는 요청을 필터 체인으로 동작합니다. 세션을 사용 하는 필터는 SecurityContextPersistenceFilter와 SessionManagementFilter가 있는데 간략화해서 SecurityContextPersistenceFilter의 동작에 대해서만 기술하려고 합니다.
스프링 시큐리티에서 인증된 사용자의 정보를 저장하는 곳 입니다. 인증된 사용자는 Context에 정보가 채워지게 됩니다. ThreadLocal로 구현되어 있습니다.
이 필터는 필터 체인 중에 상위에 위치하여 SecurityContextRepository 인터페이스로 부터 얻는 정보로 SecurityContextHolder를 채워 줍니다. 코드를 보면 아래와 같은 순서로 수행됩니다.
SecurityContextRepository에서 SecurityContext를 가지고 옴. 불러온 SecurityContext를 SecurityContextHolder에 설정.
체인 필터 수행.
SecurityContextHolder 클리어.
저장되어 있던 SecurityContextHolder를 SecurityContextRepository에 저장.
기본적으로 SecurityContextRepository는 HttpSession을 사용하는 HttpSessionSecurityContextRepository으로 설정되어 Session에 저장하게 됩니다. 요약하자면, 인증 필터들이 수행되기 전에 인증 정보를 세션에서 꺼내서 설정 해주고 필터가 수행되면 다시 세션에 저장해 줍니다. 그렇다면… 세션에 설정한다는 것이 어떤 의미를 가지고 있을까요?
나무위키에는 아래와 같이 설명하고 있습니다.
웹 서버에서 임시로 클라이언트의 데이터를 갈무리하는 것을 뜻한다. 쿠키와 역할이 비슷한데, 쿠키는 클라이언트 측에 데이터를 갈무리하는 반면에 세션은 서버 측에 데이터를 갈무리해 놓는다는 차이점이 있다. 주로 로그인, 온라인 쇼핑몰의 장바구니 등에 쓰인다.
클라이언트의 정보를 서버에 저장하는 것 정도가 되겠습니다. 세션은 파일, DB, In-Memory, Redis 등등에 저장할 수 있는데 우리가 쓰는 Undertow에서는 InMemorySessionManager를 기본으로 사용하여 In-Memory에 세션을 저장합니다. 다시 위의 요약으로 돌아가서 세션에서 꺼내 인증 상태로 만드다는 것은 클라이언트와 서버에서 사용하는 HTTP 프로토콜은 무상태성(Stateless)을 가지고 있는데 동일한 세션이고 이미 인증되었다면 인증된 상태로 만들어 주는 것이라고 할 수 있겠습니다.
우리 플랫폼에서 인증은 펠릭스가 만들어 준 람다에서 처리하고 결과를 모든 클라이언트 요청은 헤더에 담아서 보내줍니다. 그렇기 때문에 지금에야 드는 생각이지만, 요청 끼리 인증 결과를 공유하지 않아도 될 듯 합니다. 세션에서 인증된 결과를 불러와 SecurityContextPersistenceFilter뒤의 인증 필터들(UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter)에서의 인증 시도를 건너뛴다고 하더라도 인증 필터에서 오래 소요되는 작업을 하는 것이 아니기 때문에 큰 영향은 없을 듯 합니다. (타암아웃을 설정했을 때와 하지 않았을 때의 TPS를 측정해 보면 명확할 것 같습니다.)
찾아보니 Spring Security Config에는 SessionManagement에서 세션 관련 설정을 할 수 있도록 되어 있습니다. SessionManagement에는 설정 필드들 중에는 최대로 생성할 수 있는 세션 수를 설정하는 maximumSessions와 (기본값은 제한이 없습니다.) 세션 생성방식을 설정하는 sessionCreationPolicy가 있습니다. sessionCreationPolicy에는 아래와 같은 값을 설정할 수 있습니다.
ALWAYS - 항상 HttpSession을 생성합니다.
NEVER - HttpSession이 있을 때만 사용하고 만들지는 않습니다.
IF_REQUIRED - 필요할 때만 HttpSession을 만듭니다. (기본 값)
STATELESS - HttpSession을 만들지 않고 사용하지도 않습니다.
HttpSession을 만들고 사용하지 않기위해 STATELESS로 설정하려고 합니다. 값이 STATELESS일 때는 SecurityContextRepository로 NullSecurityContextRepository가 선택됩니다. NullSecurityContextRepository는 아래와 같이 SecurityContextPersistenceFilter의 필터에서 아무런 일도 하지 않습니다.
그럼 이제 테스트를 해봐야겠습니다.
테스트
커뮤니티 DEV를 대상으로 저번과 같이 댓글 조회하는 API를 호출하려고 합니다. Scouter 띄우고 nGinder로 부하를 줘 세션 타임아웃 기본 값(30m)으로 두었을 때와 적게 조절 (5m) 했을 때, 그리고 세션을 설정하지 않았을 때의 TPS와 Heap Memory 사용율을 확인해 보려고 합니다. nGinder에서 500명의 사용자로 10분간 계속 호출 하도록 합니다. JAVA_OPTS는 아래처럼 설정했습니다. CMS GC를 사용합니다.
지난 번에 테스트 했던 상황입니다. Heap 메모리가 점차 차오르다가 6분정도가 지났을 때 TPS가 급감했습니다.
Heap 메모리가 차는 6분 전에 세션이 만료되기 때문에 TPS가 급감하는 현상이 나타나지 않았습니다.
TPS는 큰 차이는 없었고 Heap 메모리가 요청에 따라 증가하는 현상이 없었습니다.
끝으로
SecurityContextHolder가 원인인줄 알았는데 Session에서 메모리를 많이 차지하는 현상으로 보입니다.
인스턴스 사양을 변경하기 전에 세션 설정을 변경해야 겠습니다.
그런데 실제로 Session을 사용하는 어플리케이션에서는 이런 현상을 어떻게 처리해야 할까요? 메모리를 많이 잡거나 타임아웃을 조절하는 수 밖에 없을까요?
더 이상 메모리 문제는 없었으면 좋겠습니다.
-
내가 만든 프로젝트 중에는 에서는 2개 이상의 DataSource를 사용하고 있는 것이 있다. Aurora RDS의 Reader, Writer 엔드포인트에 연결되는 DataSource를 만들고 사용하고 있다. 이 방법에 대한 실효성은 논외로 하고, 그 프로젝트에서 최근에 모종의 이유로 Spring Boot Actuator에 대한 의존성을 제외했다. 그런데 모듈을 제외하고 빌드를 수행하니 DataSource Bean의 순환 참조 오류가 발생했다. 문제를 해결하기 위해 우선 HikariConfig Bean을 생성하는 것으로 변경했지만 모듈의 포함 여부가 DataSource의 생성에 영향을 주는 것이 굉장히 의아했기 때문에 그 이유를 분석해 보고 결과를 남겨보려고 한다.
오류 메시지와 사용했던 코드를 간략화한 것은 아래와 같다.
순환 참조 에러는 경험한 적이 있기 때문에 어떤 상황에서 발생하는지 알고 있었다. 하지만, DataSourceInitializerInvoker가 왜, 그리고 어디서 DataSource를 참조하는지를 몰랐기 때문에 이 클래스의 역할에 대해 알아보았다. Spring 문서에는 다음과 같은 설명이 있다.
InitializingBean#afterPropertiesSet()에서 schema-*.sql을 실행하고 DataSourceSchemaCreatedEvent에서 data-*.sql 스크립트를 실행하여 DataSource 초기화를 처리하는 Bean이다.
설명을 적어보면 DataSourceInitializerInvoker는 DataSource의 초기화를 위해서 2가지 인터페이스를 구현하여 스키마 생성과 빈 생성 시점에 초기화 할 수 있다. 그리고 DataSourceInitializerPostProcessor에 의해서 DataSource 생성 후에 만들어진다. 역할과 생성 시점을 알고 나니 DataSource를 참조할 법도 해 보인다. 좀 더 명확한 이유를 알기위해 에러가 발생하는 곳을 디버깅을 해 보았다.
에러가 발생한 메소드는 Singleton Bean생성 전에 호출되는 콜백으로, 코드에서 보듯이 singletonsCurrentlyInCreation라는 Set에 현재 생성하려는 Bean 이름이 있다면 BeanCurrentlyInCreationException을 발생시킨다. singletonsCurrentlyInCreation에는 아래의 목록을 포함하고 있었고 communityDataSource가 이미 존재하기 때문에 에러가 발생한 것이다.
에러가 난 곳으로부터 trace를 따라가 보니 DataSourceInitializerInvoker가 구현하고 있는 InitializingBean인터페이스의 afterPropertiesSet메소드에서 DataSource를 가져오려고 한 것을 알 수 있었다. 그리고 @Primary 어노테이션에의해 communityDataSource가 선택되면서 순환 참조 오류가 발생하게 된 것을 알 수 있었다. 그렇다면 Actuator가 있을 때는 왜 발생하지 않았을까?
Actuator가 포함되었을 때 singletonsCurrentlyInCreation에는 위와 같은 항목들이 있었다. 이 목록에서는 communityDataSource가 없었기 때문에 에러가 발생하지 않았다. 이 사실로 writerCommunityDataSource가 communityDataSource가 아닌 다른 어떤 것에 의해 생성되는 것을 알 수 있다. 모듈 포함 여부에 따른 DataSource 생성 플로우는 아래와 같이 진행된다.
Actuator가 있을 때 5번째 과정에서는 순환 참조 에러가 왜 발생하지 않는지 모호하게 느껴졌었는데 디버깅을 해보니 DataSourceInitializerInvoker는 Bean 생성 후 호출되는 메소드이기 때문에 communityDataSource에서 의존관계를 찾을 때 정상적으로 찾는다. 생성하고 있는 Bean을 다시 참조하게 될 때만 순환 참조 에러가 발생하게 된다.
그렇다면 Actuator가 포함되었을 때는 Configuration에서 어노테이션으로 생성되는 시점보다 더 빨리 만들어져야 에러가 발생하지 않을 텐데 DataSource Bean을 생성하는 시점이 어떻게 다른 걸까?
AbstractApplicationContext에서 빈이 생성되는 시점이 다른 것을 확인할 수 있었는데 Actuator가 포함되지 않았을 때는 finishBeanFactoryInitialization메소드에서 entityManagerFactory로 DataSource가 생성되고 Actuator가 포함될 때는 그보다 앞션 onRefresh메소드의 DataSourceHealthContributorAutoConfiguration에서 생성되는 것을 확인할 수 있었다.
그런데, 테스트하다 보니 스프링 부트 버전을 변경하니 Actuator가 포함되어 있어도 순환 참조 에러가 발생하는 것을 발견했다. 디버깅해보니 DataSourceHealthContributorAutoConfiguration는 DataSource를 생성할 때 DefaultListableBeanFactory에 정의된 beanDefinitionNames에서 DataSource 타입을 찾는데 에러가 발생하는 버전에서는 communityDataSource가 먼저 생성되어 아래와 같이 순환 참조 에러가 발생하게 된다.
버전에 따라 생성 순서가 바뀌는 이유를 Configuration클래스의 Bean메소드의 메타데이터를 읽는 ConfigurationClassParser클래스의 retrieveBeanMethodMetadata메소드의 주석에서 찾을 수 있었다.
불행하게도, JVM의 표준 리플렉션은 같은 JVM 에서 동일한 어플리케이션의 다른 실행 간에도 임의의 순서로 메서드를 반환합니다.
이 내용에 따라 Configuration클래스에서 Bean이 선언된 순서를 바꾸면 에러가 나던 버전에서는 에러가 발생하지 않고 에러가 발생하지 않던 버전에서는 에러가 나는 것을 확인 할 수 있었다.
지금까지 조사해본 내용을 요약해보면 아래와 같다.
DataSource에 의존 관계가 있을 때 DataSourceInitializerInvoker에서 선택되는 DataSource에 따라 순환 참조 에러가 발생 할 수 있다.
Actuator가 포함되었을 때 에러가 나지 않던 이유는 DataSourceHealthContributorAutoConfiguration에서 DataSource를 생성해서 순환 참조 에러가 발생하지 않았다.
하지만, 생성하는 순서에 따라 에러가 발생할 수 있으며 JVM에서 순서는 보장하지 않는다.
나의 경우에는 이 문제를 해결하기 위해 서두에 언급했듯이 DataSource의 의존관계를 없애고 DataSource를 1개만 생성하는 방법을 사용했다. DataSource를 생성하고 참조했던 이유가 @ConfigurationProperties가 설정된 필드는 변경하지 않고 Bean을 생성해주기 때문이었는데 DataSource를 생성하지 않고 HikariConfig만을 생성해 기존에 하고자 했던 것은 유지하도록 했다. 이제 에러가 발생하는 원인을 알았으니 다른 방법도 충분히 사용할 듯한데 DataSourceInitializerInvoker도
여러 개 정의되어 DataSource를 찾아올 수 있다면 순환 참조 에러가 발생하지 않을 것이다.
물론 해보지는 않았다.
- 현재 플랫폼을 개발하고 운영하면서 몇 차례의 메모리 누수 문제를 겪었는데 이것들을 해결했던 과정과 그 원인을 적어보려고 한다. 발생한 지 몇 개월이 지난 것들이지만 혹시 나와 같은 문제를 겪는 사람이 있다면 도움이 되었으면 좋겠다.
가장 처음으로 발생했던 메모리 관련 문제는 서비스를 오픈하고 많은 날이 지나지 않았을 때였던 걸로 기억한다. 주말에도 마음 편하게 쉬지를 못하는 때였으니까...
아니나 다를까 정확한 선후 관계는 기억이 나지 않지만, 서비스가 느려진 것을 인지하고 각 서비스 상태를 모니터링했었다.
이 그래프에서 보다시피 특정 인스턴스 몇 대가 무슨 짓을 하는지 갑자기 CPU Utilization이 치솟는 현상이 있었고 이 서비스로 HTTP Call을 하는 다른 컴포넌트에도 영향을 미치고 있었다. 암담한 심정과 더불어 도무지 원인을 알 수가 없었는데 갑자기 CPU가 치솟는 현상을 경험한 적이 없었기 때문이다. 그래서 문제가 있는 인스턴스들을 재시작하였고 서비스 상태는 잠잠해진 듯했다. 문제가 언제 다시 발생할지 몰라 조마조마하긴 했지만.
계속 모니터링하다 보니 시간이 지나면 문제의 현상이 재현되었고 LB에서 제외하여 해당 인스턴스에서 문제를 파악해 보기로 했다. top 명령으로 보았을 때 java가 CPU를 가장 많이 점유하고 있는 것을 확인 할 수 있었고 top -H -p pid로 java의 특정 thread가 CPU를 많이 사용함을 알 수 있었다.
그리고 sudo -u user jstack -l pid 명령으로 jvm의 thread dump의 tid와 대조한 결과 그 thread가 GC thread라는 것을 알 수 있었다. 즉, 이유는 모르겠는데 GC를 수행하느라 CPU를 많이 쓰고 있고 GC가 수행되어도 메모리 해제가 되지 않았기 때문에 현재 상태에 도달했을 것이다. 이때부터 메모리 누수가 의심되기 시작했다. (그때 jstat을 사용했다면 좀 더 의미 있는 상태 분석을 했을 것 같다.)
메모리 누수가 의심되는 상황이기는 했지만 역시나 원인이 짐작도 되지 않았다. 특별히 메모리를 많이 요구하는 코드도 없었기 때문이다. 그래서 메모리 분석을 위해 jvm의 heap dump를 받아보기로 했다. sudo -u user jmap -dump:format=b,file=dump.bin pid 명령을 사용했다.
dump는 시간이 꽤 걸렸는데 메모리 사용량에 따라 다르겠지만.. 기억하기로는 2G일 때 2시간 정도 걸렸던 듯하다. 시간이 걸리는 작업이기 때문에 작업을 걸어놓고 다른 것을 하다가 나중에 확인했을 때 터미널이 끊어져 있는 불상사를 맞이하고 nohup으로 백그라운드에서 수행한 기억이 있다. 그리고 AWS EC2에서는 package를 설치해야 jmap 실행이 가능했다.
덤프 파일 분석에는 eclipse의 MAT(Memory Analyzer)를 사용했다.
분석해보니 PartTreeJpaQuery라는 클래스에서 사용하는 ConcurrentHashMap이 메모리를 많이 차지하고 있었다. 관련된 내용이 있는지 검색해 보았고 나의 증상과 비슷해 보이는 Spring Data JPA 이슈를 발견할 수 있었다.
이 이슈에서는 JPA repository의 findBy...의 파라미터로 Pageable을 사용할 때 메모리 누수가 있는 것 같다고 제보하고 있었다. 그리고 이 이슈는 장애가 발생한 날의 불과 4일 전에 버전 올림 되면서 수정되었다. PR을 보면 이전에 넣은 기능을 롤백하는 것인데 DATAJPA-1575으로 2019년 8월 5일 2.2.0.RC2 버전에 포함되었다.
정리하면 2019년 8월 5일에 Spring Data JPA의 2.2.0.RC2에 추가된 기능을 2020년 1월 15일에 2.2.4와 2.3.0 버전 이상에서는 이전 코드로 되돌려 해결하였다. 문제가 있었던 프로젝트는 Spring Boot 2.2.0을 사용하고 있었고 댓글을 조회하고 페이징하기 위해 JpaRepository를 상속한 인터페이스에서 findBy...에 Pageable을 파라미터로 사용하고 있었다.
같은 코드에서 Spring Data JPA 버전만을 변경하여 heap 메모리 사용을 비교해 보기로 했다.
환경은 Java 1.8을 사용하였고, Heap의 최초, 최대 크기는 1G로 설정했다. 그리고 nGinrder로 100명의 가상 유저로 요청을 시뮬레이션하였으며 Scouter로 모니터링하였다. 각 요청은 아래의 코드를 수행하게 했다.
Spring Data JPA 2.2.0
시작 후 30분가량 후에 힙 사이즈가 최대치인 1G에 도달하였고 GC로도 메모리가 회수되지 않아 점차 사용 가능한 메모리가 줄어드는 모습을 보여주었다. 사용 가능한 메모리가 줄어들면서 TPS도 급감하였다. (날짜가 변경되어서 그런지 Scouter에서 Heap 메모리 로그가 전부 보이지 않는데 100M부터 점차 사용량이 증가하였다.)
Spring Data JPA 2.2.4
2시간 정도 수행하였는데도 600M 아래로 사용하고 있고 TPS 저하 현상도 보이지 않았다.
위의 PR에서 보다 시피 이전 코드는 ParameterBinder를 생성하고 Map에 저장하여 재사용하려고 했고 변경된 코드는 ParameterBinder를 생성한 뒤에 별다른 작업 없이 바로 반환해주고 있다. 이전 코드에서는 생성 비용을 줄이려고 Map에 저장한 뒤에 재사용하는 것을 의도했지만 키로 사용한 ParameterMetadata객체의 동일성을 보장해주지 못해 메모리 누수가 발생한 것으로 보인다. 실제로 debugger로 확인했을 때 동일한 인수임에도 Map의 크기가 호출할 때마다 늘어나는 것을 볼 수 있었다.
https://stackoverflow.com/questions/43753568/aws-ec2-jmap-heap-dump
↩
- 내가 지금 회사에서 개발한 것 중에는 사용자에게 보이는 알림과 작품, 시스템에 대한 알림 설정을 관리하는 Notification API가 있고 댓글과 작품의 Rating, Like를 관리하는 Community API가 있다. 그런데 이 두 서비스에서 빈도는 낮지만, 간혹 CannotAcquireLockException이 발생하여 원인을 찾아보려고 했던 과정과 사례를 적어본다.
문서에는 이름과 같이 lock을 얻지 못해 발생하는 예외이다. 또한 stack trace의 메시지와 검색한 결과로 보건대 Notification API에서는 알림을 읽은 시간을 기록하는 Confirm API와 댓글을 Like 하는 Comment Like API에서 발생했고 deadlock과 관련되어 보였다.
DB는 트랜잭션을 안전하게 수행하기 위해 lock을 사용한다. 그런데 트랜잭션들이 서로 필요한 lock을 가지고 있어 진행할 수 없는 상태가 되는 것이 deadlock이다. MySQL 문서에 있는 예제를 보면 어떻게 deadlock이 발생하는지 보여준다.
클라이언트 A에서 share mode로 select하여 lock를 얻는다. share mode는 트랜잭션이 끝날 때까지 값이 변경되지 않도록 한다.
그리고, 클라이언트 B에서 트랜잭션을 시작하고 row를 삭제한다.
삭제 작업은 x lock을 필요로 합니다. 그리고 s lock이 있는 동안은 x lock을 획들 할 수 없다. A가 s lock을 가지고 있고 B의 요청은 queue로 전달된다.
끝으로, A가 row를 삭제하려고 한다.
deadlock은 이때 발생한다. A는 삭제 작업을 하기 위해 x lock이 필요하지만, B가 x lock에 대한 요청을 기다리고 있고 A가 lock을 해제하기를 기다리고 있기 때문이다. 결과적으로 InnoDB는 클라이언트 중 하나의 lock을 해제하고 에러를 발생시킨다.
Confirm API는 알림을 읽은 시간을 기록하는 API이다. 30일 이내의 최대 50 건의 알림을 가져와서 읽지 않은 알림만 현재 시각으로 갱신하는 로직이다. (SimpleJpaRepository의 saveAll()을 사용한다.)결국, SELECT와 UPDATE하는 로직인데 UPDATE 할 때는 x lock이 필요하겠지만 위의 예제처럼 SELECT할 때 share mode를 쓰거나 하지 않아 s lock이 필요하지 않다. 그렇다면 왜 발생한 걸까?
SHOW ENGINE INNODB STATUS 명령어의 LATEST DETECTED DEADLOCK 항목에 아래의 내용이 있었다.
이 내용으로 알 수 있는 사실은 다음과 같다.
TRANSACTION 1은 id가 199064878인 row를 UPDATE 한다.
TRANSACTION 2는 id가 199063150인 row를 UPDATE 한다.
TRANSACTION 1은 UPDATE하는 record에 대한 x lock이 필요하여 기다리고 있다.
TRANSACTION 2는 UPDATE하는 record에 대한 x lock이 필요하여 기다리고 있다.
TRANSACTION 2는 TRANSACTION 1이 기다리고 있는 record의 x lock을 가지고 있다.
이 과정대로 쿼리를 수행하면 재현이 가능하다.
테이블을 생성.
위와 같이 데이터를 입력.
첫 번째 클라이언트에서 id가 1인 record의 x lock 획득.
두 번째 클라이언트에서 id가 2인 record의 x lock 획득.
그리고 id가 1인 record의 x lock을 얻기 위해 대기.
다시 첫 번째 클라이언트에서 id가 2인 record의 x lock 획득 시도.
여기서 deadlock발생.
재현 방법은 어렵지 않은데 위의 과정처럼 하나의 트랜잭션에서 2개 이상의 UPDATE명령을 실행해야 발생한다. 하지만 나는 트랜잭션을 사용한 적이 없었는데 deadlock이 발생한 것이 의아했다. 그러다가 saveAll() 코드를 확인해 보았는데 여기에 트랜잭션 어노테이션이 사용되고 있었다.
이제는 deadlock이 발생했을 때의 상황을 알 것도 같다.
짦은 시간 안에 Confirm API가 동시에 호출되어
읽은 시간을 업데이트하기 위해 알림 목록을 saveAll()로 업데이트할 때
같은 알림목록을 대상으로 하여도 업데이트가 수행되는 순서는 보장되지 않기 때문에
서로 다른 트랜잭션에서 lock을 필요로하는 상황으로 deadlock 발생한 것이다.
Comment Like API는 댓글의 좋아요 버튼을 누를 때를 호출되는 API이다. comment, comment_like 2개의 테이블을 사용한다. comment_like에는 사용자가 댓글에 좋아요 설정 또는 취소한 상태를 comment에는 댓글과 좋아요 숫자를 필드로 가지고 있다. 아래는 2개의 테이블을 간략화한 스키마이다.
이것도 SHOW ENGINE INNODB STATUS로 deadlock 정보를 찾아보았다.
Confirm API와 유사하지만 두 개의 트랜잭션이 하나의 row를 업데이트하고, 두 번째 트랜잭션이 x가 아닌 S lock을 가지고 있는 것이 눈에 띈다. 이 로직에서도 명시적으로 lock을 선언한 부분이 없는데 왜 deadlock이 발생한 것일까?
InnoDB에서 lock을 설정하는 경우를 나열해놓은 문서에서 Foreign Key 조건에서 s lock을 설정한다고 나와있다.
FOREIGN KEY가 테이블에 정의되어 있다면 제약조건을 확인해야 할 모든 insert, update, delete는 제약조건을 참조하고 있는 record에 shared lock을 설정한다.
또한, InnoDB는 제약조건이 실패하는 경우에도 lock을 설정한다.
이 내용대로 추론해보면 아래의 과정으로 재현할 수 있다.
deadlock 테이블을 참조하는 deadlock_child 테이블 생성.
첫 번째 클라이언트에서 자식 테이블에 insert하여 deadlock 테이블의 id가 1인 record의 s lock 획득.
두 번째 클라이언트에서 동일하게 deadlock 테이블의 id가 1인 record의 s lock 획득.
첫 번째 클라이언트에서 x lock 획득을 위해 대기.
두 번째 클라이언트도 x lock이 필요. 여기서 deadlock 발생.
이것도 짧은 시간 안에 같은 API가 동시에 요청되어 발생한 문제라고 여겨진다.
이것 또한 MySQL 문서에서 deadlock를 어떻게 최소화할 수 있는지 알려준다. 기본적으로는 deadlock이 발생할 때는 재시도 하라고 하고 있으며 트랜잭션을 짧게 설정하라고 하고 있다. Confirm API의 경우에는 하나의 트랜잭션에서 수행되는 쿼리가 최대 50개가 될 수 있기 때문에 UPDATE ... WHERE IN 으로 변경하려고 한다. (2개의 row만 업데이트하는 경우에도 deadlock이 발생하기도 했다.)
Comment Like API는 부모 테이블을 먼저 업데이트하면 deadlock이 발생하지 않는다. 쿼리에 필요한 lock을 미리 획득하기 때문인데 순서를 변경하여도 로직에는 영향을 주지 않기 때문에 순서를 바꿔도 무방하다.
Deadlock found when trying to get lock; try restarting transaction
↩
https://jeong-pro.tistory.com/94
↩
- RESULT
- RESULT
- 오랜만의 코딜리티... 근 2달만이다.
그 동안 여러가지 일이 있었다.
RESULT
- RESULT
- RESULT
내가 이해한 바를 적어보면, 문제는 주어진 수들의 합이나 차의 절대값이 가장 수를 찾는 것이다. 바꿔말하면 주어진 집합을S, 부분집합을 U 라 하고, S 에서 U 를 제외한 나머지 수의 집합인 C 두 부분으로 나눌때 ∣SUM(U)−SUM(C)∣ 가 가장 작은 것. 즉, 가능한 부분집합들을 찾고 그 부분집합과 나머지 집합의 차가 가장 작은 수를 찾는 것이다.
부분집합의 차를 구하기 위해 집합의 모든 수를 다 더한 전체합을 이용한다.
전체합에서 구하려는 부분집합에 2를 곱하고 빼서 차를 구한다.
예를 들어, S={a,b,c,d,e} 라고 하고 U={a,b,c} 라고 할 때 C={d,e} 이고
⇒a+b+c+d+e−2(a+b+c)
⇒d+e−(a+b+c)
이므로 S와 C의 차를 구할 수 있다. 그리고 부분집합을 선택 할 때 a,b,c 를 선택하는 경우와 d,e 를 선택하는 경우의 차가 동일하기 때문에 SUM(U)<SUM(S)/2
까지만 계산 할 수 있다.
부분집합을 구할 때에는 위에서 설명한 바에 따라서 두 부분의 부분집합 중 합에 대한 부분만을 찾아도 되며 그 부분집합의 합이 SUM(S)/2 인 것까지만 찾으면 된다. 그런데 문제에서 주어진 집합에 길이에 비해 집합의 수의 범위가 작으므로 중복된 수가 많이 발생할 것이 예측가능하므로 계산 시간을 줄이기 위해 부분합을 구할 때 집합의 원소의 갯수를 활용하는데 부분합과 같이 기록하며, 부분합을 구하고 남은 숫자를 의미한다.
집합의 원소들을 SUM(S)/2 까지 순회하면서 부분합을 찾는데, 이전 단계들에서 만든 부분합일 때는 현재 단계에서 사용할 수 있다는 의미로 카운팅을 넣고 이미 구한 부분합으로 만들 수 있는 부분합이라면 숫자 하나를 더 사용했다는 의미로 이전 단계에서 1을 뺀다.
- RESULT
- RESULT
- RESULT
- RESULT
- 그냥 이렇게 풀면 안 되나 해서 풀었던 방법.
RESULT
Caterpillar Method를 사용한 방법.
성능은 이 경우는 양 끝단에서 범위를 줄이기 때문에 더 빠르다.
RESULT
- RESULT
-
RESULT
- RESULT
Codility의 문제들은 RESPECTABLE 정도만 되어도 참 어렵다.
TLE가 왜 나는지 모르겠어서 한참을 헤맸다.
못을 위치 순으로 정렬 한 후에(문제의 조건이 널빤지를 다 박을 수 있는 못의 인덱스 이므로 이전의 순서를 기억하게 한다.), 널빤지를 순회하면서 못을 박을 수 있는 가장 작은 인덱스를 찾고, 찾은 인덱스들 중에 가장 큰 수가 전체 널빤지를 박을 수 있는 최소의 못의 수이다.
그리고 아직도 직관적으로 이해되지는 않지만, 널빤지를 순회하면서 찾은 가장 작은 인덱스가 이전에 찾은 인덱스보다 작을 경우는 더 탐색할 필요가 없기 때문에 종료 조건을 걸어야 시간초과를 피할 수 있다.
아 하나 더, 못의 위치와 인덱스를 저장하기위해 Object의 List로 만들고 정렬해서 사용 했었는데... 배열을 사용하지 않으면 시간 내에 통과하지 못한다.
- RESULT
- RESULT
- 최근에 회사에서 작은 규모로
2명이서 스터디를 하고 있다. 나는 파이썬을 배우고, 스프링에 대해 궁금한 점을 나에게 물어보면 간략히 조사해서 발표하는 식으로 진행하고 있는데 적정한 테스트 코드 커버리지 범위에 대한 얘기가 나와서 그것에 대해 한번 찾아보다가
재밌는 글
을 발견해서 아래에 적어본다.
Testivus On Test Coverage
어느 이른 아침, 한 프로그래머가 프로그래밍의 달인에게 물었다.
"저는 몇 가지의 유닛 테스트 코드를 작성하려고 합니다. 어느 정도의 코드 커버리지를 목표로 해야 합니까?"
달인이 대답했다.
"커버리지에 대해서는 걱정하지 마시고, 좋은 테스트를 쓰면 되느니라."
그 프로그래머는 웃으면서 인사한 후에 떠났다.
...
그 다음날, 두 번째 프로그래머가 같은 질문을 했다.
달인이 물이 끓는 솥을 가르키면서 말했다.
"솥에 쌀을 얼마나 넣어야 하는가?"
그 프로그래머는 의아해하면서 대답했다.
"제가 그걸 어떻게 압니까? 당신이 얼마나 많은 사람을 먹일 것이며, 그들이 얼마나 배고프며, 다른 음식은 주었는지, 쌀이 얼마나 있는지 등등에 달렸죠."
"바로 그것일세." 거장이 대답했습니다.
두 번째 프로그래머도 웃으면서 인사한 후에 떠났다.
...
하루가 끝날 무렵, 세 번재 프로그래머가 와서 코드 커버리지에 대한 같은 질문을 했다.
"80% 이하로!" 달인 단호한 목소리로 그의 주먹으로 책상을 내리치면서 대답했다.
세번째 프로그래머도 웃으면서 인사한 후에 떠났다.
...
마지막 대답 후에, 제자가 달인에게 다가왔다.
"스승님, 저는 오늘 코드 커버리지에 대한 같은 질문에 다른 3개의 대답을 들었습니다. 왜 그렇게 하셨습니까?"
달인이 그의 의자에서 일어섰다.
"나와 차 한잔 마시면서 이야기를 해보자꾸나."
그들이 컵에 뜨거운 녹차를 채운 후에 거장은 대답하기 시작했다.
"첫 번째 프로그래머는 테스트를 이제 막 새롭게 시작하는 단계였다. 그는 지금 많은 코드를 가지고 있지만, 테스트는 없지. 그는 갈 길이 멀지.
이때 코드 커버리지에 초점을 맞추는 것은 지치게 만들고 쓸모없는 것이지.
그는 몇 가지 테스트를 작성하고 실행하는 것이 더 나을 것이야. 코드 커버리지에 걱정하는 것은 나중 일이지."
"반면에 두 번째 프로그래머는 프로그래밍과 테스팅에 상당한 경험을 가지고 있지. 내가 그녀에게 쌀을 얼마나 솥에 넣어야 하냐고 물어봄으로써
필요한 테스트의 양은 여러 가지 요소에 달려있다는 것을 깨닫게 해주었지. 그리고 그녀는 요소들에 대해 자신의 코드니까 나보다 더 잘 알 것이다.
단순한 하나의 답은 없다. 그녀는 그 사실을 감당하고 다룰만큼 충분히 똑똑했지.
"그렇군요." 젊은 제자가 말했다. "하지만 단순한 하나의 답변이 없다면 세번째 프로그래머에게는 80%이하라고 대답하셨습니까?"
달인은 아주 크게 웃었고 녹차 때문만은 아닌 이유로 배가 위아래로 출렁거렸다.
"세 번째 프로그래머는 하나의 쉬운 답변을 원했지. 단순한 대답은 없음에도 불구하고 말이야... 어쨌든 대답을 따르지 않을 걸세."
어린 제자와 머리가 희끗희끗한 달인은 조용히 사색에 잠겨 차를 마셨다. - RESULT
어렵다.
원하는 값을 찾기 위해 이진탐색을 적용한 것도 생각지도 못했으며
구간으로 찾는 값을 구성할 수 있는지 판별하는 로직도 직관적으로 와닿지 않는다.
한달 뒤에 다시 푸려면 풀 수있을까?
- RESULT
- RESULT
처음에 작성했던 코드는 성능에서 까였는데 개구리가 도착해도 탐색을 계속했기 때문.
최소의 점프 횟수를 찾는 것이기 때문에 처음 도착했을 때 종료하도록 했다.
- RESULT
이해가 잘 안가는 어려운 문제다.
참고했던 것 중에는 요 설명이 가장 낫다.
요약하자면,
소수의 약수 집합이 같으려면 두 집합이 서로에게 속해야한다.
최대 공약수를 빼면서 포함되고 있는지를 포함한다
수를 소인수분해된 형태로 이해하면 좀 편하다.
- RESULT
- 발등 골절 후 2달이 지난 후에 써보는 후기.
귀찮지만 심심해서(?) 써본다.
사건 발단
2018년 6월 23일날 토요일... 김포에서 하는 결혼식을 가기위해 아침 일칙부터 일어났었다. 와이프 친척의 결혼식이였기 때문에 장인 ᛫ 장모님이 우리 집에 오셨고
새로 산 카시트를 차에 싣기위해 카시트를 들고 지하 1층 주차장으로 내려갔으나 차가 없었다. 그렇다면.. 지하 2층에 있겠거니하고 계단으로 가려고 생각했었다.
~(그런 생각은 하지 말았어야 했는데)~ 계단을 거의 다 내려왔을 때 쯤 마지막 발을 내딛었을 때 오른발을 접질렸다.
가장 먼저 든 생각은 역대급으로 심하게 접질렸다고 생각했고 통증이 있었지만 잠깐 그러겠거니하고 운전대에 올랐다. 결혼식장에서도 절룩거리면서 음식은 가져다 잘 먹었고
돌아오는 길에도 통증이 계속 되고 집에 도착해서 신발을 벗어보니 발이 곰발바닥 마냥 부어있었다. (이 때부터 뭔가 잘못됬다고 느낀거 같다.) 토요일이 였기 때문에 응급실로 가게되었고 골절을 진단받으며 반깁스와 목발신세를 지게되면서 지금까지도 고통을 받고있다.
사건 경과
내 다리는 반깁스에서 → 깁스 → 보조기를 거쳤고 1달이 넘게 목발과 한 다리로만 살았다. (다친 다리가 점점 얆아지더라..) 반깁스로는 2주, 깁스는 4주 정도 하고 있었던 것 같다. 다치고 나서는 다리가 땡땡 부어서 잘 때는 다리에 배게를 두고 잤다. 조금만 올리고 있어도 붓기가 확실히 금방 빠졌다. 다리가 부었을 때는 저릿저릿하고 앉아 있지도 못하겠더라. 다리는 처음 겪어보는 골절인데 정말 더디게 낮는다. 하루단위로는 잘 느껴지지 않고 일주일 정도로는 낮긴 낮는데 평소에는 쉽게 하던 것 들도 하지 못해 도대체 언제 걸을 수 있나하는 답답함이 많이 들었다. 깁스를 푸르고도 바로 걷지도 못했다. 의사는 걸으라는데 걸을 수 있어야 걷지 아파 죽겠는데... 나는 주말이여서 시내의 종합병원 응급실로 간 후 그 곳에서 진료를 계속 받았는데 다른 곳으로 갈걸 그랬다. 병원과의 거리도 그렇고 결정적으로 의사 선생님이 별로였다. 골절이라 뭐 특별히 진료 해주는 것은 없겠다만 지금 내 상황과는 조금 다른 판에 박힌 말을 하는 느낌이나 보조기를 구입하라고 권유한 것 점 등 병원을 다닐 수록 신뢰가 많이 깍이는 느낌이였다.
출 ᛫ 퇴근도 참 힘들었다. 차를 얻어 타거나 택시타고 지하철역까지 간 후 20분 정도를 가야하는데 운이 나빠 자리에 앉지 못할 때가 있는데 그냥 지하철 바닥에 앉아서 갈까 하는 생각이 많이 들었다. 대부분의 사람들이 목발을 짚고있어도 알아봐 주고 비켜주거나 하지는 않았다. 물론 고맙게도 그렇게 해주신 분들도 있었다. 주로 노약자석 쪽에 탔었는데 오히려 일반석 보다 노약자석에 앉아 계신 분들이 더 잘 비켜주기도 했다. 그 사람들이 밉거나 하지는 않다. 애초에 그럴 것이라 생각하고 아무런 기대도 하지 않았고 대부분 핸드폰을 보느라 아예 보지 못한 사람이 대다수 일 것이다. 아무튼 간에 힘들어서 마침 일도 없고 그만두겠다고 얘기 한 후기도 해서(...) 일 있을 때만 출근하겠다고 하고 일주일에 2 ~ 3번만 출근했었다.
사건 결말
아직 다 낮지는 않았다. 그렇지만 이제는 보통 걸음걸이의 속도를 회복했고 걸었을 때의 통증도 거의 없지만 발이 붓는 현상은 아직 있다. 그리고 발목의 가동 범위가 줄었고 요상하게도 발끝으로 딛었을 때의 통증이 있다. 발을 다치고 여러가지를 느꼈지만 여러모로 몸 아프면 정말 나만 고생한다는 것과 가장의 무게를 다시 한번 느끼게 되었다. 목발이 조금만 물기가 있어도 미끄러지는 건지 지하주차장에서 목발이 미끄러져 크게 넘어진 적이 있었는데 아픈 것도 아픈 거지만 서러워서 주저 앉아서 목놓아 울고싶었다. 100년만의 최악의 여름이였다는데... 이 보다 더 나쁜 여름은 내 인생에서 없었으면 한다.
- RESULT
-
역시 회사에서 굴러다니던 책.
AWS에 대한 관심은 있었고 AWS상에서 작업해야될 일이 있을 것 같아서 보게 되었다.
주로 사용하게 되는 서비스들(IAM, S3, VPC, EC2, RDS, ELB, CloudFront)에 대한 간단한 소개와 과금 방식 그리고 실습 위주로 구성된 입문용 책이다.
아마존에서 매월 교육을 진행해서 몇 차례 다녀 왔는데 이것 말고도 AWS는 배울 수 있는 자료가 많은 것 같다.
- RESULT
러닝타임을 어떻게 줄일 수가 있지 고민했는데 반감법(bisection)을 요렇게 적용하는 것이구나
RESULT
- RESULT
- RESULT
- RESULT
- RESULT
- RESULT
각 위치를 경계로 미리 구해놓은 전과 후의 최대값의 부분을 더해 그 중에서의 최대값을 구하는 로직.
-
자바 ORM을 책으로 공부해야겠다는 생각은 늘 하고 있었는데 마침 회사에 굴러다니던(?) 책이 있어서 보기 시작했다.
책의 내용은 XML이나 어노테이션을 활용하여 Artist, Track, Album등의 객체를 모델링을 하고 HQL과 크리테리아 쿼리, 그리고 스프링에서 사용할 수 있는 방법을 소개하고 있다.
설정 방법 부터 직접 따라할 수 있는 코드가 많아 입문용으로 적합할 듯한 서적이다.
사실 나의 경우는 이 책만으로는 하이버네이트에를 잘 이해할 수 없었기 때문에 조만간 책 한권을 더 봐야 할 것 같다는 생각이 들었다.
예제를 따라하다가 LazyInitilizeException이 발생했고 해결을 위해 검색하다 OSIV(Open Session In View)에 대해서도 알게 되었고 안티패턴이니 하는 논쟁과 더불어
스프링 부트에서는 기본적으로 적용되어 있다는 데 왜 나는지를 찾게 되었다.(에러는 lombock에서 생성된 toString()으로 인하여 발생했었다.)
새로운 기술에대한 책이나 글을 접할때마다 느끼는 거지만(이미 회자되는 기술일 수록 많이) 나는 이걸 언제 써보나 하는 생각이 들 때가 많다. 하이버네이트는 예전 부터 한번 잘 사용해 보고 싶다는 생각을 가지고 있었는데 아주 다행스럽게도 신규 프로젝트를 진행할 일이 생길 것 같아 많이 다뤄 볼 수 있을 것 같다.
- RESULT
첫번째 방법는 Map을 이용해 숫자가 나타나는 갯수를 카운팅하여 Leader값을 계산하고
두번쨰 방법은 Leader가 되는 수는 다른 수들 보다 반 넘게 나타난다는 점을 이용해서 Leader값을 계산 하는 방식이다.
그리고 두 부분으로 나눈 경우에도 전체의 Leader값과 동일하다는 것을 이용한다.
내 생각으로는 Map과 일반 계산식으로의 시간 복잡도가 O(n) 으로 동일 할 것 같았는데 아니였다.
다른 언어의 솔루션에서는 이런 방법으로 풀었을 때 통과하는 것 같은데...
아무튼 자료를 참고하여 풀었기 때문에 나중에 다시 풀면 풀 수 있을까 싶다.
언젠가는 꼭 material을 번역하면서 차근차근 다시 봐야겠다.
RESULT
- RESULT
- RESULT
- RESULT
- RESULT
- RESULT
- 난이도가 MEDIUM만 되어도 꽤나 어렵다. 우선 완전 탐색부터...
원의 오른쪽 보다 다른 원의 왼쪽이 같거나 작을 때 교차된다고 판단한다.
RESULT
여러 솔루션들이 있었는데 개인적으로는 이 방법이 가장 직관적인 것 같았다.
왼쪽 좌표를 정렬 한 뒤에 오른쪽 좌표보다 작은 좌표는 교차한다고 판단하는 방법이다.
오른쪽 좌표보다 작은 좌표를 찾을 때 이진탐색으로 찾는다.
정렬할 때 Stream API로 정렬했었는데 역시나 속도가 안나온다.
RESULT
- RESULT
- RESULT
- RESULT
- RESULT
보통의 방법으로 루프를 사용하여 O(N∗M)으로 해결하는 방법.
아래는 각 문자열에서 나타낸 갯수를 세어 각 문자에서의 문자 갯수 스냅샷을 기록한다.
그리고 문제의 시작, 종료 인덱스의 스냅샷의 값이 낮은 순서로 문자 갯수를 빼서(해딩 구간에 문자가 나타났다면) 결과를 구하는 방식이다.
RESULT
- RESULT
시뻘건 퍼포먼스의 점수를 보고 여기서 O(N2)이 아니게 어떻게 짜야하는지 답이 안 나와 결국 검색...
결론은 전체 2, 3개의 부분집합의 평균이 전체의 평균보다 작다는 것을 이용하는 방법이 있었다.
RESULT
- RESULT
- RESULT
A보다 큰 K로 나누어 떨어지는 수를 찾고 그 수와 B사이의 수를 찾는 전략을 사용했다.
이것도 조건을 찾느라 꽤나 고생했는데 풀이를 보고는 머리의 한계를 통렬히 느꼈다...
RESULT
- RESULT
- RESULT
- RESULT
- RESULT
- RESULT
내가 왜 굳이 이렇게 짰을까?
RESULT
- RESULT
이런 신묘한 방법이...!
RESULT
- RESULT
문제를 잘못 읽어 배열 크기 산정을 잘못했다.
RESULT
다른 풀이를 보지 않았다면 생각해 낼 수 있었을까?
RESULT
- RESULT
- 예전에 작업했던 스크립트 라이브러리를 정리하면서 (역시나 왜 하고 있나 하는 자괴감이 들지만) 그 때 생각하면서 몇 자 적어본다.
첫 회사에 입사하고 3개월 정도 수습 기간이 끝나고 였나... 느닷없이 자바스크립트 라이브러리 개발 TF로 발령(?) 되게 된다. 그리고 과장 좀 보태면 내 인생은 그렇게 바뀌게 되었다. 전에 신입사원들 대상으로 자바스크립트 개발부장이 스터디를 시킨 적이 있는데 그것이 전조였던 거 같고 그중 내가 뽑히게 되었었다. 그때가 2012년도였으니까 자바스크립트의 주가가 치솟던 때였을 것이고 뭔가를 해보고 싶으셨던 게 아닐까 싶다. 여하튼 TF 구성원은 나 포함 주니어 2명, 시니어 1명, 마크업 담당 1명, 총괄 1명 총 5명이었고 처음 취지는 업무 오버헤드가 마크업에서 항상 걸려 개발이 늦어지니 비 개발자들도 HTML만 가지고 페이지 구성을 할 수 있게 해보자는 것이었다. 취지는 나쁘지 않았다고 생각한다. 취지만...
결론부터 얘기하자면 취지와는 다르게 사용되었다. 시연도 여러 번 했었지만, 기획자나 디자이너가 원하는 것과는 다르다는 생각이 많이 들었다. (그들은 대부분 HTML조차 만지고 싶어하지 않는다.)비 개발자들을 위한 라이브러리였지만, 개발자들이(주로 내가) 사용했다. 기존 페이지는 대부분 정적 페이지였는데 정적으로 서비스하기 까다로운 페이지들, 예를 들면, 개편될 때 기존 스타일로 서비스되는 문제를 해결하기 위해 과거 기사 페이지들에 사용되었고, 태블릿, 모바일 앱의 웹 뷰에 사용되었다. (그때 총괄 부장님이 소극적으로 사용되던 것에 한이 맺혔었는지 추후에 이걸 가지고 웹 페이지 저작 툴을 만드는 프로젝트를 하게 된다. 이것도 할 얘기가 참 많지만 다음에...)
결론은 대충 저렇고 과정은 정말로 우여곡절도 많았고, 답답하기도 했고, 술이 많이 늘었었다. 물론 배운 것도 참 많다. 자바스크립트에 대한 나의 인식을 완전히 바꾼 계기가 되었고, 지금은 프론트엔드를 UI 개발 말고는 할 일 없어 소홀하지만 자바스크립트는 마음의 고향 같은 느낌이다. 이때 더글라스 크락포드의 Javascript:The Good Parts를 정말 감명 깊게 봤었다. 언어의 장점, 단점을 작가가 명확한 논지로 설명하는데 카타르시스가 느껴질 정도였다. (그리고 얇다.)그리고 가디언이나 뉴욕타임스에 기술 블로그들을 보는 것도 좋았고 공부할 수 있는 시간이 참 많았었다.
결국, 미래가 없을 것 같은 프로그램을 계속 개발해야 하는 것에 지쳐서 이직하게 되었고, 그 때 TF에 참여한 사람들은 나를 포함해서 전부 지금은 자의든 타의든 그만두거나 다른 회사에 재직중이다. 가끔 그 때를 생각하면 추억보정이 되는건지 아련한 느낌이 많이 든다. 조만간 광화문에 술이나 한 잔 하러 가야겠다.
블로그 정리하다가 다시 읽어보니까 왜 이렇게 오그라드냐... 지우고싶지만 남겨둔다.
이 블로그를 개설한 이유는 나를 최대한 GEEK한(?) 사람처럼 보이게 하기 위해서다. 원래 나는 블로그에 글 쓰는 것도 정말 귀찮아하고 이런 짓 할 시간에 누워서 TV나 보는 사람인데, 이력서를 쓰다가 홈페이지란을 비워두고 싶지 않아서 만들게 되었다. 그리고 그 이미지 포장작업의 일환으로 JSON Resume로 이력서를 작성해보았다. 내가 이력서까지 왜 JSON으로 작업하고 있나하는 회의감이 들었지만 어쨌든 간에 하고나니까 뿌듯한 건 있다. 테마로 light-classy-responsive 라는 테마를 조금 변형해서 사용했는데, 로컬 테마로 퍼블리싱하는 기능은 제공하지 않는다.
서른 살 쯤 부터는 내 자신을 잘 파악하게 되어 내가 꾸준히 글을 안 쓸 것 이라는 것을 누구보다도 잘 안다. (실제로 이 파일을 만든 날짜와 수정날짜가 다르다. 그렇기 대문에 쓰고 싶었던 내용과 지금 쓰고 싶은 내용이 다르다.) 주로 쓰게 될 내용은... 올해는 최대한 책을 많이 읽기로 하였으니 독서 일기를 많이 쓰게 될 것 같다.