아직 해결중입니다. 버그가 쉽게 해결되지 않아서 스트레스가 밀려오지만 또 면접에서 할말 생겼다는 생각이 들어서 긍정적으로 생각하기로 했습니다. 집에서 공부하니까 집중이 잘 안되는 거 같아 나가려다 영하 17도를 보고 다시 담요를 여몄습니다. 집 세탁기가 얼었습니다. 다행히 보일러는 아직 살아있네요.
발생한 버그에 대한 로그는 아래와 같습니다.
Connection leak detection triggered
Connection leak detection triggered for com.mysql.cj.jdbc.ConnectionImpl@
HikariPool에 대해서 나오고 있어서 Hikari가 문젠가 하고 먼저 들여다 보았습니다.
오늘은 설연휴 첫날입니다. 게더에 접속했는데 생각보다 사람들이 많이 없어서 놀랐어요. 공부를 좀 해야겠습니다. 이제 정말 3주도 안남았거든요. 하차를 많이 한다고 했는데 하차를 할 이유는 이제 없어졌습니다. 이제 이 부트캠프에 투자한 돈과 시간이 가치가 있었는지를 검증할 일만 남았네요.
내년 설연휴에는 어떤 모습으로 TIL을 쓰고 있을지 궁금해지네요. 인생이 5G로 달리고 있는 느낌입니다.
아침부터 차례지내고 친척들과 담소를 나누니 피곤해서 뻗었습니다. 저녁에는 오랜만에 친구들과의 약속이 있어서 나갔다 왔습니다. 대학교친구들인데 어느새 저희가 만난지도 10년이 다되어 간다고 하며 세월이 너무 빠르다고 했습니다. 제가 부트캠프 한다고 작년 가을에 말했었는데 그것도 이제 3주도 안남았습니다.
근데 재밌는건 다른 친구들도 진로 변경에 대한 고민을 하고 있다는 거였습니다. 지금 나이가 과도기인거 같아요. 다들 뭐든 잘 할 수 있어서 더욱 더 고민이 되는 것 같습니다. 하나만 잘하는 게 아니라 여러개를 잘하는데 여러개에 흥미가 있어서 여러가지를 도전할 수 있는 상황인거에요. 지금 저희 세대는 평균 수명이 100세가 넘어가서 앞으로 최소 80년은 더 살아야하고 직업을 3~4개 가질 거라고 합니다. 저는 이미 하나는 클리어 했고 2개째 입니다. 고용되는 직업으로서는 그렇습니다.
웹개발자는 전세계에 내가 만든 것을 선보일 수 있다는 점에서 더 재미가 있습니다. 디지털 세상에 내 영토를 만드는 것이라고 생각하면 개발자는 매력적인 직업입니다.
Exclusive Lock은 데이터를 변경하고자 할 때 사용되며, 트랜잭션이 완료될 때까지 유지되어 해당 Lock이 해제될 때까지 다른 트랜잭션은 해당 데이터에 읽기를 포함하여 접근을 할 수 없습니다.
Shared Lock은 다른 사용자가 동시에 데이터를 읽을 수는 있지만 Write는 할 수 없습니다.
PESSIMISTIC_WRITE은Exclusive Lock을 걸고, 데이터를 읽거나, 업데이트하거나, 삭제하는 것을 막습니다.
JPA는 PESSIMISTIC_WRITE 이외에도
PESSIMISTIC_READ- Shared Lock을 걸고 데이터가 업데이트 되거나 삭제되지 않도록 한다
PESSIMISTIC_FORCE_INCREMENT - PESSIMISTIC_WRITE와 유사하고 ENTITY의 버전 속성을 추가로 증가시킨다.
EXCEPTION의 경우에는 PersistenceException(락을 가져오는데 실패하면 예외 발생), LockTimeoutException(락을 기다리다가 설정해놓은 wait time을 지났을 때 예외 발생) 이 있습니다.
저는 좋아요 실행 시 충돌이 발생한 것을 이미 봤기 때문에 충돌이 발생한다는 것을 가정하는 비관적 락을 걸었습니다. Lock이 길어지면 Latency가 길어지기 때문에, @QueryHints를 사용하여 Lock에 3초 타임 아웃을 적용했습니다.
결과는?
잘 됩니다.
좋아요를 여러번 연속해서 처리할 때 true 값만 반환이 되던(아마 충돌이 일어나서 실행이 되지 못했겠던) 현상이 사라지고 정상적으로 잘 작동이 됩니다.
프론트 요청 처리하기
새로운 칼럼을 추가하려고 하니 이미 DB가 서버에 올라가 있어서 다 날리고 새로 올리는 작업을 여러번 반복해야했습니다. 저만 칼럼을 수정하고 추가하고 있던게 아니었거든요.. API 명세가 처음이랑은 전혀 다른 모습이 되었습니다. 내일이 중간 발표인데 중간 발표가 끝나면 한번 정리하는 시간을 가져야 할 것 같습니다.
이제는 CRUD 수정하는 정도는 금방 해버립니다. 실제로 위의 1,2,3, 요청은 2 시간도 안걸려서 해결해버렸답니다. JPA가 정말 편해요.. 스프링은 사랑입니다😘
로그인을 하지 않은 유저가 게시글을 상세 조회 했을 때, 좋아요 여부와 팔로우 여부는 false 값으로 처리가 되는 로직을 구현해야했습니다. 그리고 member 값이 null 로 들어가도 nullpointerException이 발생하지 않게 예외 처리를 해줘야했습니다.
게시글과 콜라보 조회 시에 발생하는 문제였습니다.
member 값을 받아와야 해당 유저의 정보를 확인해서 팔로우를 했는지, 좋아요를 했는지를 알 수 있는데 로그인을 하지 않은 유저라면 member 값을 불러 올 수가 없어서 null로 뜨게 되서 NullPointerException이 발생합니다. 아래와 같이 NullPointerException을 처리했습니다.
Controller
@GetMapping("/details/{postid}")
public ResponseEntity<SuccessResponse<Object>> infoPost(@PathVariable Long postid, @AuthenticationPrincipal CustomUserDetails userDetails){
Member member = null;
try {
member = userDetails.getMember();
}catch (NullPointerException e){
log.info("비로그인 사용자 접근 : infoPost");
}
return SuccessResponse.toResponseEntity(INFO_POST,postService.infoPost(postid, member));
}
@Slf4j 을 사용해서 log를 찍었습니다.
try-catch문을 사용하여서 NullPointerException 이 발생하면 log를 띄워주었습니다.
member 값은 null로 초기화 했습니다.
Service
@Transactional
public InfoPostDto infoPost(Long postid, Member member) {
Post post = postRepository.findById(postid).orElseThrow(
() -> new NotFoundException(POST, SERVICE,POST_NOT_FOUND)
);
post.viewCount();
postRepository.save(post);
Boolean isLiked = false;
if(member!=null){
Member postMember = memberRepository.findByNickname(post.getNickname()).orElseThrow(
() -> new NotFoundException(POST, SERVICE, MEMBER_NOT_FOUND)
);
PostLikeCompositeKey postLikeCompositeKey
= new PostLikeCompositeKey(member.getId(), postMember.getId() );
if (postLikeRepository.findById(postLikeCompositeKey).isPresent()){
isLiked = true;
}
}
return new InfoPostDto(post, isLiked);
}
if문을 사용해서 member가 null이 아닌 경우, 좋아요 여부를 반환합니다.
null 인 경우에는 초기값인 false를 반환합니다.
이번주 토요일에 중간 발표가 있습니다. 여러모로 착착 진행되고 있습니다. 프론트와 소통해서 자잘한 에러를 수정하거나 api를 수정하고 있습니다.
웹개발자가 되겠다고한지 벌써 5개월차입니다. 친한 지인이 퇴사하고 부트캠프 할거라고 해서 이미 고연봉자인 지인이 한다니 호기심이 생겼던 게 지난 9월이었습니다. 가족이 새로 하는 사업 때문에 백수였던 제가 도와드리고 있었는데 '웹사이트를 만들어봐라' 하셔서 돈주고할려다가 너무 비싸서 제가 만들기 시작하고 있었던 것도 한 몫 했습니다. 웹개발 툴(아임웹)을 쓰고 있었는데 간단한 html 코드 정도 써보고 있던 수준이었습니다. 부트캠프의 존재도 모르고 있었는데 어떻게 벌써 이렇게 한달밖에 안남은 시점이 되었네요.
TIL
이제는 CRUD 정도는 척척하게 되었습니다. 처음에는 "왜 다 안알려주고 이렇게 불친절한 부트캠프를 봤나!" 하고 살짝은 불만이었던 부분들이 어느정도 해소가 되었습니다. 하다보면 자연히 필요에 의해 찾아볼 수 밖에 없습니다. 그래도 저는 여전히 제가 하고 있는 항해99를 추천하냐고 묻는다면 공부를 아주 열심히 해본 적 있는 분들만, 어느정도 이과적 공부 머리가 있는 분들에게 추천합니다. 뭐... 12시간동안 앉아서 공부할 의지가 있다는 거 자체가 남들과는 다른 의지와 공부 습관을 가진 사람들이라고 할 수는 있겠다는 생각이 듭니다. 부트캠프 신청자체가 진입장벽이라고 생각하면 괜찮을 수도 있겠네요. 저는 원래 공부만 하던 사람이라 괜찮았습니다. 스스로 찾아서 하는 개발자가 되는게 중요하다고 했는데 저는 원래 하던 일도 그래서 괜찮았습니다.
친절하고 열심히하는 동료들을 만나는 것도 큰 행운으로 작용해서 제가 부트캠프에 열정적으로 임하게 되는 요인중에 하나입니다. 저는 아직까지 항해99의 동료들에게 불만이 있었던 적이 없습니다. 다들 너무 감사하고 제가 작년에 있던 불운들은 이분들을 만나기 위해 행운을 아껴놓았던 거라고 생각할 정도입니다. 최종 프로젝트는 6주동안 함께 하는 사람들인데 다들 열정적이고 저보다 코딩 배운지 오래 되신 분들이 대부분이라 배울점이 너무 많은데 친절하시기까지 하고, 새로 들어온 디자이너님도 열정 있으시고 실력도 뛰어나서 또 내게 이런 행운이 왔구나하는 마음에 감사했습니다. 저 또한 이분들에게 좋은 동료가 되고 싶습니다.
남은 한달동안은 최종 프로젝트를 잘 마무리하고 이력서도 쓰고 코딩 테스트 준비도 하고 면접준비도 하면 금방 지나갈 것 같습니다. 지금까지 해왔던 것처럼 앞으로도 꾸준히 무소의 뿔처럼 나아가겠습니다.
영어를 할 줄 아는 개발자가 장점이 많을 거라는 생각이 드네요. 에러 메세지가 한글로 뜨면 참 좋을텐데 말이죠.
아 물론 저는 잘합니다😎
Stacktrace
stack trace는 프로그램 서브루틴에 대한 정보를 제공하는 보고서입니다. 일반적으로 stack trace을 통해 소프트웨어 엔지니어가 문제가 있는 위치 또는 실행 중에 다양한 서브루틴이 함께 작동하는 방식을 파악하는 데 도움이 되는 특정 종류의 디버깅에 사용됩니다.
그리고 저의 일주일 회고를 이야기하는데 이번에 구글링을 하면서 블로그 자료들 중에 정확하지 않은 것도 많고 구린것도 많아서 정보를 보는 눈을 키우고 싶다고 말씀드렸더니 공식문서를 읽으라고 하셨습니다. 부트캠프에서 학생들에게 강제하다보니 기술블로그도 우후죽순 생겨나서 그렇다고 하셨습니다. 앗 근데 저도 괜히 반성하게 되서 기술 관련해서 리뷰할 때는 좀 더 정확하게 써야겠다고 다짐했습니다.
오늘은 알림기능을 위해서 먼저 구현해야하는 좋아요를 복합키를 사용해서 구현해보려고 합니다. 게시글 좋아요, 댓글 좋아요가 필요한데 복합키를 써보는 경험을 다 같이 해보기 위해서 저는 게시글 좋아요 다른 팀원분이 댓글 좋아요를 맡았습니다. 복합키 자체가 자료가 잘 안나오네요. 조장님이 복합키 사용해보라고 했는데 한번 도전해봅니다. 일단 뭔지 알아야겠지요.
Composite Key 복합키
복합키는 Composite Key로 2개 이상의 column을 프라이머리 키로 가지고 있습니다. 복합키는 @IdClass와 @EmbeddedId 어노테이션을 사용해서 정의할 수 있습니다. 복합키는 다음과 같은 규칙을 가지고 있습니다.
SseEmitter 클래스는 SpringFramework에서 버전 4.2 부터 제공합니다.
emitter는 발신기라는 뜻을 가지고 있습니다.
String emitterId = memberId_System.currentImeMillis(); 로 한 이유는 Last-Event-ID와 관련이 있습니다.
Last-Event-ID는 클라이언트가 마지막으로 수신한 데이터의 Id값을 의미합니다. 그러나 Id 값만을 사용한다면 언제 데이터가 보내졌는지, 유실 되었는지 알 수가 없기 때문에 시간을 emitterId에 붙여두면 데이터가 유실된 시점을 알 수 있으므로 저장된 Key값 비교를 통해 유실된 데이터만 재전송 할 수 있습니다.
findAllEmitterStartWithByMemberId 는 해당 member와 관련된 모든 emitter를 찾습니다.
findAllEventCacheStartWithByMemberId 는 해당 member와 관련된 모든 event를 찾습니다.
EmitterRepositoryImpl
위의 EmitterRepository를 아래와 같이 구현합니다.
public class EmitterRepositoryImpl implements EmitterRepository {
private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
private final Map<String, Object> eventCache = new ConcurrentHashMap<>();
@Override
public SseEmitter save(String emitterId, SseEmitter sseEmitter) {
emitters.put(emitterId,sseEmitter);
return sseEmitter;
}
@Override
public void saveEventCache(String emitterId, Object event) {
eventCache.put(emitterId,event);
}
@Override
public Map<String, SseEmitter> findAllEmitterStartWithByMemberId(String memberId) {
return emitters.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(memberId))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
@Override
public Map<String, Object> findAllEventCacheStartWithByMemberId(String memberId) {
return eventCache.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(memberId))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
@Override
public void deleteById(String emitterId) {
emitters.remove(emitterId);
}
@Override
public void deleteAllEmitterStartWithId(String memberId) {
emitters.forEach(
(key,emitter) -> {
if(key.startsWith(memberId)){
emitters.remove(key);
}
}
);
}
@Override
public void deleteAllEventCacheStartWithId(String memberId) {
eventCache.forEach(
(key,emitter) -> {
if(key.startsWith(memberId)){
eventCache.remove(key);
}
}
);
}
}
동시성을 고려하여 ConcurrentHashmap을 사용합니다.
HashMap은 멀티스레드 환경에서 동시에 수정을 시도하는 경우 예상하지 못한 결과가 발생할 수 있습니다. 멀티스레드 환경하에서 HashMap을 안전하게 사용하기위해 java에서는 concurrent 패키지를 제공합니다. ConcurrentHashmap을 사용하면 thread-safe가 보장됩니다. ConcurrentHashmap 관련 설명은 여기 블로그를 참조하세요.
그냥 사용한다면 ConcurrentModificationException이 발생할 수 있습니다.
오늘부터 MVP 2차를 개발하기 시작했습니다. 알람 기능 구현이 제가 맡은 부분입니다. 프론트엔드가 2명밖에 없어서 알람까지 할 수 있을지는 모르겠지만 일단은 만들고 생각하기로 했습니다. 프론트엔드분들도 우는소리 하시면서 척척 해내시더라고요. 저희 프론트엔드분들도 그렇고 백엔드분들도 능력자들만 있어서 참 팀원운이 좋다고 생각합니다.
구현해야하는 기능
1. 승인요청알람
2. 승인완료알람
3. 게시글 좋아요 알람
4. 댓글 좋아요 알람
5. 댓글 등록 알람
처음에는 웹소켓으로 해야하나라고 생각했는데 서버에서 클라이언트에 알람을 보내는 단방향 형식이기 때문에 SEE라는 것을 사용하기로 했습니다. 웹소켓(WebSocket)은 클라이언트와 서버 간의 효율적인 양방향 통신을 실현하기 위한 구조입니다. 채팅과 같이 클러이언트와 서버가 양방향 통신이 필요하면 웹소켓을 쓰겠지만 알람은 서버만 클라이언트에게 정보를 보내는 구조입니다.
추후에 DM을 구현하게 된다면 웹소켓을 사용할지 다시 검토해봐야 겠습니다.
SSE(Server-Sent-Events)
전통적으로 웹에서는 새 데이터를 수신하기 위해 서버에 요청을 보내야 합니다. 클라이언트가 서버에 데이터를 요청합니다. SSE를 사용하면 서버가 클라이언트에 메시지를 푸시하여 언제든지 새 데이터를 클라이언트로 보낼 수 있습니다. 이러한 수신 메시지는 클라이언트의 이벤트 + 데이터로 처리될 수 있습니다.
SSE는 웹소켓과 비슷하지만 단방향 통신입니다. 클라이언트는 서버로 데이터를 보낼 수 없습니다. 웹소켓과 달리 별도의 프로토콜을 사용하지 않아서 훨씬 가볍다고 합니다. HTTP 스트리밍을 통해 서버에서 클라이언트로 단방향의 Push Notification을 전송할 수 있는 HTML5 표준 기술입니다.
SSE는 서버와 한번 연결을 맺고나면 일정 시간동안 서버에서 변경이 발생할 때마다 데이터를 전송합니다.
위의 mozilla.org 사이트에서 보여준 예시는 자바스크립트인데, 자바스크립트에서는 EventSource를 이용해서 구현할 수 있습니다. Spring Framework 4.2 부터 SseEmitter 클래스를 제공해 SSE 통신을 구현할 수 있습니다.
아침에는 오랜만에 헬스를 다녀왔습니다. 부트캠프 시작하고 몸무게는 변동은 없지만 체성분이 변했습니다. 스쿼트를 40kg 10x3 set 밖에 못해서 놀랐습니다. 근육 놀랄까봐 무게를 더 치진 않았습니다. 2월 17일에 부트캠프가 끝나는데 다시 스쿼트랑 데드 70 들던 때로 돌아가겠습니다. 개발자지망생이 되니 다시 거북목이 심해져서 내일은 등이랑 어깨를 뿌실겁니다. 그리고 알람은.... 내일까지 해보자!!!
제가 맡은 도메인이 바로 이 콜라보리퀘스트입니다. 깃헙의 pull request에서 영감을 받은 기능입니다.
현재 CRUD의 CR만 구현된 상태입니다. 승인을 위해 보여지는 상세 페이지와 승인이 완료된 콜라보리스트를 보여주는 기능을 각각 구현했습니다.
수정 기능과 삭제 기능은 오늘 만들어 놓으려고 합니다.
승인되기 전에만 수정 또는 삭제 할 수 있게 합니다.
위의 기능들은 CRUD이기 때문에 기존에 했던 프로젝트와 크게 다르지는 않습니다.
그래서 제가 욕심을 내고 있는 기능은 알람기능입니다.
원하는 기능은 아래와 같습니다.
1. 콜라보 리퀘스트를 작성하면, 해당 게시글의 작성자에게 승인 요청 알람 보내기
2. 승인이 완료되면, 해당 콜라보 리퀘스트 작성자에게 승인 완료 알람 보내기
CRUD가 끝나면 깊이 파볼 생각입니다.
List 수정 하는 법
삭제하는 법은 querydsl을 사용해서 처리했는데 수정은 모르겠어서 일단은 삭제 후 새로 저장하는 방법으로 구현했습니다.
수정 시, musicList의 music의 갯수가 늘어나거나 줄어드는 경우가 있어서 뭔가 복잡한 느낌이 듭니다.
결국 알아낸 방법은 똑같이 query를 수정해주면 된다는 것이다.
삭제할 때는 다음과 같은 코드를 사용해서 musicList를 삭제해주었다. Music은 CollaboRequest를 외래키로 가지고 있다.
@Transactional
@Modifying
@Query("DELETE from Music c where c.collaboRequest = :collaboRequest")
void deleteAllByCollaboRequest(@Param("collaboRequest") CollaboRequest collaboRequestId);
QueryDSL
문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
클린 코드에 대한 이야기가 나왔는데 멘토님이 SOLID 원칙을 말씀해주셔서 찾아보았습니다.
로버트 C. 마틴이 2000년에 쓴 논문 Design Principles and Design Patterns에서 software rot에 대해 설명하면서 처음 소개된 개념입니다.
S Single responsibility principle (단일책임원칙) - 하나의 클래스는 하나의 책임만 가진다. O Open-closed principle (개방-폐쇄원칙) - 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다. L Liskov substitution principle (리스코프 치환 원칙) - 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다. I Interface segregation principle (인터페이스 분리 원칙) - 특정 클라이언트를 위한 인터페이스 여러 개가 인터페이스 하나보다 낫다. D Dependency Inversion principle (의존관계 역전 원칙) - 프로그래머는 "추상화에 의존하며, 구체화에 의존하면 안된다."
2004년에는 Michale Feathers에 의해 SOLID 라는 약어로 소개되었습니다.
SOLID 원칙들은 어떠한 객체지향 디자인에도 적용할 수 있습니다.
애자일 개발 또는 Adaptive software development(ASD)의 방식의 핵심 철학이 될 수 있습니다.
솔직히 이번주차는 개인적으로 중요한 큰 이슈가 있어서 부트캠프에 100% 집중하지는 못했습니다.
하지만 맡은 역할은 다 구현을 했고 지금은 관련해서 refactoring을 하고 있습니다.
mapstruct도 처음 써봤고 직접 swagger를 적용하는 것도 처음입니다.
다른 분들의 저와는 달리 깔끔해보이는 코드를 보면서 반성도 하고 제 꺼에 적용도 해보면서 발전해나가고 있습니다.
생각보다 API 명세서를 수정해야하는 부분이 많이 보여서 처음에 설계한 대로 구현하긴 했지만 refactoring 해야할 부분들이 많이 보입니다.
저희 프로젝트는 백엔드 보다는 프론트엔드 부분의 역량이 중요해서 백엔드에 대해서 어떤 도전적인 모습을 보여줄 수 있을까 고민도 했는데 굳이 고민안해도 될 것 같습니다. 생각보다 구현이 어려운 부분이 많습니다.
그래도 부트캠프 처음 들어왔을 때와 지금을 비교하면 천지차이라고 할 수 있습니다.
간단한 CRUD는 그냥 합니다.
이번주차에서 어려웠던 것은 한 request안에 여러 Entity들에 들어가는 값들을 받아와서 따로 그 Entity에 맞는 repository에 저장을 해주는 것이었습니다. 반대로는 (다른 Repository에 있는 정보를 하나의 Response Dto에 넣어서 보내주기)는 했는데 반대를 어떻게 해야할 지에 대한 고민이 있었습니다.
결론은 한 requestDto에 여러 entity의 값들을 받아와서 controller에서 두개의 dto로 나눠 service에 전달을 하는 거였습니다.
코드 공유를 살짝 하자면 아래와 같습니다. MapStruct는 사랑입니다.
@PostMapping("/api/post/{postid}/collabo")
public ResponseEntity<?> collaboRequest(@PathVariable Long postid, @RequestBody RequestCollaboRequestDto requestCollaboRequestDto, @ApiIgnore @AuthenticationPrincipal CustomUserDetails customUserDetails){
collaboRequestService.collaboRequest(postid, requestCollaboRequestDto.tocollaboRequestDetailsDto(), requestCollaboRequestDto.tosaveMusicDto(), customUserDetails.getMember());
return SuccessResponse.toResponseEntity(COLLABO_REQUEST, null);
tocollaboRequestDetailsDto() 와 toSaveMusicDto()로 RequestCollaboRequestDto로 받아온 값들을 나눠서 service에 전달해주었습니다.
지금은 처음에 Musicfile를 하나만 작성할 수 있었는데 API 명세를 보니 조회할때는 musicList로 받아와서 여러개를 requestDto에 담을 수 있도록 수정하고 있습니다. 오늘 이거는 해놓고 퇴근하려고 합니다.
1. 한 메서드에 오직 한 단계의 들여쓰기만 한다. 2. else 키워드를 쓰지 않는다 3. 모든 원시값과 문자열을 wrap 한다. 4. 한 줄에 점을 하나만 찍는다. 5. 줄여쓰지 않는다. 6. 모든 entity를 작게 유지한다. 7. 2개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다. 8. 일급 컬렉션을 쓴다 9. getter/setter/property를 쓰지 않는다