본문 바로가기
728x90
728x90

전체 글191

테스트 속도 개선 및 리팩토링! 테스트 너무 느려 요즘 카페서비스는 좀 더 스마트한 테스트 환경을 위해 Testcontainer를 사용하고 있다. 그런데, Testcontainer의 유일한 단점. 테스트 실행시 컨테이너가 실행 되는데, 이 때문에 테스트에 걸리는 시간이 늘어났다. 꼴랑 167개 테스트 돌리는데 58초.. 거의 1분이 걸린다. 테스트 속도를 개선 할 수는 없을까? 개선 1: Application Context 스프링 테스트 프레임워크에는 컨텍스트 캐싱 이라는 기능이 있다. 한 번 로드 된 애플리케이션 컨택스트를 캐시에 저장하고, 동일한 설정과 환경을 가진 후속 테스트에서 해당 캐시 된 컨택스트를 재사용 하는 기능이다. 근데 저 말을 잘 생각해보면, 후속 테스트에서 이전 테스트와 다른 설정과 환경을 갖는 다면 Applica.. 2023. 10. 6.
테스트 컨테이너를 사용하는 이유 + 테스트 격리 도커도 불편해 요즘 카페서비스는 MySql을 사용하고 있는데, 테스트 환경와 운영환경을 최대한 일치 시키기 위해 테스트 역시 MySql을 사용하고 있다. 처음에는 도커 컨테이너를 띄워서 테스트를 돌렸다. 물론 로컬 MySql을 사용하는 것 보다는 관리포인트가 줄어들지만, 이 또한 몇 가지 불편한 점들이 있다. 테스트를 돌리려면 DB 컨테이너를 실행 시켜줘야 한다. 해본 사람은 알겠지만, 이게 은근히 거슬린다. compose 파일 관리도 비용이다. 프로젝트 초반에는 여러 이유들 때문에 compose 파일 변경이 잦았는데, compose 파일을 관리하는 것 역시 비용이 들어가는 행위라는 것을 깨달았다. port를 신경 써야 한다. 맥북을 이틀 간 수리를 맡긴 적이 있었다. 이틀 동안 놀수는 없어서 여자친구 .. 2023. 10. 6.
ID가 있는 엔티티를 save() 할때 발생하는 SELECT 쿼리 없애기 INSERT 하려면 SELECT 2번 나감 최근에 인덱스를 한번 정리하고 싶어서, 서버의 API로 요청이 들어올 때 발생하는 모든 쿼리를 리스트업 했었다. 그리고 다음은 새로운 유저가 OAuth를 통해 로그인 할 때 발생하는 쿼리들이다. Member를 SELECT 하는 쿼리가 두번 발생한 이후에 INSERT 쿼리가 발생한다. 우리 서비스는 로컬 회원가입이 없다. 구글과 카카오를 통한 소셜 로그인만 가능한데, 사용자가 로그인 버튼을 누르고 사용자가 선택한 provider로 부터 사용자의 정보를 받는다. 위 정보를 통해 우리 서비스에 등록된 사용자라면 로그인을, 등록되지 않은 사용자라면 등록 후 로그인을 진행한다. @Transactional public TokenResponse createAccessToke.. 2023. 9. 27.
N+1 해결하기 (발생하는 쿼리 80% 줄이기) N + 1 ORM을 사용하다 보면 무조건 만나게되는 문제가 있다. 바로 N+1. 우리 서비스 역시 N + 1 문제를 마주쳤다. 카페조회 @Entity public class Cafe { @Embedded private Images images; @Embedded private Detail detail; } @Embeddable public class Images { @ElementCollection(fetch = FetchType.LAZY) @CollectionTable(name = "image") private List image; } @Embeddable public class Detail { @ElementCollection(fetch = FetchType.LAZY) private List avai.. 2023. 9. 19.
공간 인덱스로 조회속도 32배 개선하기(요즘 카페 지도 기능 개발) 우리 서비스에 지도 기능을 추가하기로 하였다. 계획은 다음과 같다. 회색선이 교차되는 부분이 지도의 중심점이다. 지도의 중심점을 기준으로 원을 그리고 원 안에 포함되는 카페들을 핀으로 보여주기로 하였다. 현재 우리서비스에서 사용하는 MySql에는 공간에 대한 정보를 담을 수 있는 공간 데이터와 이 공간데이터를 효율적으로 이용할 수 있는 공간 함수, 그리고 검색의 성능을 올릴 수 있는 공간 인덱스가 존재 한다. 이것들을 이용해보려 한다. 1. MySql 공간 데이터 다루기 MySql에서 제공하는 공간데이터의 종류는 위와 같다. 단일 타입으로는 Point, LineString, Polygon 이 세가지가 있고, 나머지들은 이 세가지 타입의 조합이다. 현재 개발하려는 기능은 각 카페마다 좌표를 갖고 있어야 하.. 2023. 9. 17.
이미지 리사이즈 성능 개선하기 1. 카페 등록이 너무 느리다. 요즘 카페 서비스는 카페를 트렌디함을 사진으로 보여주는 서비스인데, 사진의 용량이 너무 크기에 화면에 랜더링되는 속도가 너무 느린 문제가 있었다. 그렇기에 이미지를 필요한 크기에 따라 리사이즈 하는 기능을 추가했었다. 그런데 리사이즈 기능을 추가하고 나니, 카페를 등록하는 요청을 처리하는 시간이 너무 오래걸린다. public List resizeAndUpload(final List files, final List sizes) { final List resizers = files.stream() .map(this::multipartfileToImageResizer) .toList(); final long start = System.currentTimeMillis(); res.. 2023. 9. 14.
Java + Spring 이미지 리사이즈 1. 이미지 리사이즈를 한 이유 몇 주전 요즘카페 서비스를 실제 사용자들에게 배포 후, 설문을 통해 피드백을 받았다. 피드백의 내용 중 압도적으로 많은 내용을 차지하는 것이 이미지 로딩 속도와 관련된 것이었다. 현재 요즘 카페에서 보여지고 있는 모든 이미지들은 최대한 예쁘고 잘나온 사진들의 원본을 EC2의 로컬에 저장해두고, 저장된 원본사진을 그대로 보여주고 있었다. 이러한 방식 때문에 이미지 로딩에 긴 시간이 걸릴 뿐 더러, 데이터 사용량 또한 상당히 높았다. 이를 해결하기 위해, 이미지 업로드 시 리사이징을 진행한 후 S3에 업로드를 하기로 하였다. 2. 리사이즈 툴 선정 우선 리사이즈를 할 툴을 선정해야했다. 고려했던 툴은 다음과 같다. marvin thumbnailator Imgscalr 2DGr.. 2023. 9. 4.
RestDocs + OAS 도입기 (feat. RestAssured) 1. API 문서 자동화의 이유 프로젝트를 진행하면서, 이전까지는 Notion을 이용해 API docs 를 작성하고 관리하였다. 하지만 당연하게도 기능개발을 하면서 몇몇 이유로 API docs를 수정해야 하는 일이 생겼다. 그때마다 노션에 들어가서 API docs를 변경하는 것은 여간 번거로운 일이 아니었다. 심지어 어떤 때는 기능개발에 정신이 팔려 API docs의 업데이트를 새까맣게 까먹고 있다가, 프론트에서 변경 전 API 주소로 요청을 하는 경우도 있었다. 해야된다는 것은 문제가 터지기 전부터 인지하고 있었으나, 기능 개발이 우선 이란 마음에 차일피일 미루다, 더 이상은 미룰 수 없다! 라는 마음으로 API 문서 자동화를 진행하기로 했다. 2. RestDocs vs Swagger 그리고 OAS 이.. 2023. 9. 1.
RestAssured & @Transactional 방금 저장한 데이터를 못찾아요 1. 조회 실패 프로젝트를 진행하며 간단한 테스트코드를 작성하다가, 원하는 대로 동작하지 않는 경우가 있었다. 다음은 문제가 발생한 테스트코드이다. @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) public class MemberTest { @LocalServerPort int port; @Autowired private MemberRepository memberRepository; @BeforeEach void setUp() { RestAssured.port = port; } @Test @Transactional @DisplayName("멤버를 조회한다") void findMemberTest() { //given final Member me.. 2023. 8. 22.
[요즘카페 트러블슈팅] orElse + JPA 콜라보로 DB가 날라감;; 우테코 레벨3 프로젝트로 진행하고 있는 요즘 카페를 진행하며 경험한 트러블 슈팅입니다. MVP까지 기능 개발을 끝낸 뒤, release를 위해 팀 내부에서 자체적으로 QA를 진행했다. 몇 가지의 테스트를 진행하던 도중 정말 이상한 버그를 발견했다. 특정 상황에서 사용자의 좋아요 목록이 초기화 되는 해괴한 버그였다. 몇번의 테스트를 통해서 좋아요 목록이 초기화 되는 플로우를 찾아냈다. 회원가입을 한다. 카페 정보에 좋아요 를 누른다. 로그아웃 후 다시 로그인 한다. 좋아요 목록이 뿅하고 사라진다...? 좋아요 목록이 사라지는 원인은 결국 DB에 있던 사용자의 좋아요 목록이 모두 다 삭제 되는 것이 원인이었는데, 왜 때문에 이런일이 일어나는지 이유를 찾기위해 위의 워크 플로우와 동일한 테스트 코드를 작성했다.. 2023. 8. 6.
Eager Loading & Lazy Loading 아직 JPA랑 좀 서먹서먹한 관계라 이 친구에 대해 잘 알지 못 한다. Eager Loading 과 Lazy Loading에 대해 처음 들었을 때는 '그게 뭔데;;' 라는 생각이 들었고, 누군가가 설명해줬을 때는 '그런게 된다고?' 라는 생각이 들었다. JPA랑 조금 더 친해져 보자. Eager 일반적인 게시판을 생각했을 때, 게시글(Post)와 사용자(Member)의 관계는 N:1 이다. // Member Entity @Entity public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; ... } // Post Entity @Entity public c.. 2023. 7. 16.
도메인 객체의 equals & hashcode (feat. ID) 장바구니 협업 미션 을 진행하며 도메인객체에 ID 필드를 넣어주었다. public class Order { private final Long id; private final Integer price; private final Member member; private final CartItems orderedItems; //... } 도메인 객체가 ID를 갖고 있을 때의 장점과 단점을 비교해 보았을 때, 장점 쪽이 좀 더 마음에 와닿았기 때문이다. 도메인에 ID를 넣어주겠다는 것은 객체의 식별자로 ID를 사용하겠다는 의미이고, ID를 통해 동등성을 보장하겠다는 의미이다. 처음에는 굳이 동등성을 보장할 필요가 없는 도메인 객체는 equals & hashcode를 재정의해주지 않았다. 필요하지가 않은데 굳이 .. 2023. 6. 7.
@Transactional(readOnly = true) 이거 왜함..? 그냥 안쓰면 되는거 아님? @Transactional 다른 크루들은 이미 많이 사용해본 듯 하지만, 나는 이번 미션(지하철)을 하면서 처음으로 @Transactional 어노테이션을 사용해봤다. @Transactional 주석을 통해 트랜잭션의 범위를 지정하는 사용한다. 트랜잭션 데이터 베이스의 상태를 변환 시키는 프로그램의 단위작업. 무슨 말이냐 하면, @Transactional 어노테이션을 사용하면, Spring은 해당 메서드에 대한 트랜잭션 관리를 자동으로 처리한다. 메서드가 호출 될 때 트랜잭션이 시작되고, 메서드가 정상적으로 완료되면 트랜잭션이 커밋되며, 예외가 발생하는 트랜잭션은 롤백된다. 만약, @Transactional 어노테이션을 클래스레벨에서 사용한다면, 해당 클래스의 모든 메서드에 대해 트랜잭션 관리를 하게된다.. 2023. 5. 22.
JSON 필드 하나면 왜 Jackson 파업함? 이번 장바구니 미션 2단계 를 진행하면서, 도저히 이해할 수 없는 에러가 하나 생겼었는데 왜 이러한 에러가 생겼고, 어떻게 해결 했는지 기록을 남겨보려 한다. 예외 발생 user 별로 장바구니를 추가하는 기능을 만들다 발생한 에러였다. 다음은 CartController의 일부이다. @PostMapping public ResponseEntity addProductToCart( @AuthenticationPrincipal User user, @RequestBody CartCreateRequest cartCreateRequest ) { final Long cartId = cartService.addProduct(user, cartCreateRequest.getProductId()); return Respons.. 2023. 5. 9.
[Spring] 200 vs 204 vs 404 자동차 미션 2단계를 진행하면서, 전체 저장내용을 불러오는 기능을 구현해야 했다. 그런데, 만약 저장된 게임이 없다면 응답을 어떻게 줘야할까? NoContent vs Empty List status code 204(no content)의 존재를 어디선가 주어 들어서 알고는 있었다. 말그대로 응답할 콘텐트가 없다는 뜻이니, 이걸 사용하면 좋겠다란 생각이 들어서 해당 status코드를 이용하여 응답을 주었다. @GetMapping("/plays") public ResponseEntity getResults() { List allResults = gameService.getAllResults(); if (allResults.isEmpty()) { return ResponseEntity.noContent().b.. 2023. 4. 24.
[Spring] @Repository vs @Component 삽질로그 결론부터 얘기하자면 @Repository 와 @Component는 bender 마다 다른 DB관련 예외를 @Repository어노테이션을 사용하면, DataAccessException으로 전환해준다고 한다. 이하의 글은 @Repository의 예외전환을 직접 확인해보고픈 욕심을 가진 의심병말기환자의 삽질 글임. 오늘 Spring Core 강의 에서 Dao에 @Repository 를 사용하냐 @Component 를 사용하냐 얘기를 나누던 도중 어떤 크루가 Dao 에 붙어있던 @Repository를 @Component로 바꿨더니, 잘 되던게 안된다고 했다. 나도 궁금해져서 해보니까 난 잘되더라. 근데 얘네 무슨 차이가 있는 걸까. 공식문서 확인 가장 신뢰도 높은 확인방법이라 생각하고 공식문서를 확인해봤다. .. 2023. 4. 23.
[Spring] @ResponseBody 무쓸모 아님?(HttpMessageConverter, ResponseEntity) 이번에 웹 자동차경주 미션 1단계를 진행 하면서 프론트에서 보낸 요청에 대해 응답을 주기 위해, @ResponseBody 어노테이션을 method에 붙여주었다. @PostMapping("/plays") @ResponseBody public ResultDto play(@RequestBody GameInfoDto gameInfoDto) { ... } 그런데 페어인 이리내가 @ResponseBody 어노테이션을 제거하면 어떻게 되냐고 물어보길래, "글쎄... 해보쉴?" 하고 어노테이션을 제거하고 실행해봤다. 그런데 예상과 달리 어노테이션의 유무와 상관없이 똑같이 동작하더라. @ResponseBody는 사실 간지용이 아닐까..? @ResponseBody 간지용인지 아닌지 확인하기 위해 뭐하는 놈인지 확인을 해봤.. 2023. 4. 14.
뒤늦게 공부해보는 함수형 인터페이스, 근데 이거 왜 씀? 우테코 레벨1 체스미션을 진행하며 함수형 인터페이스를 처음 사용해봤다. 정말 뒤늦게 공부하는 거 같긴한데.. 늦게라도 하는게 안하는 것보다는 항상 낫다고 생각한다. 함수형 인터페이스란? 함수형 인터페이스는 1개의 추상 메소드를 갖고 있는 인터페이스를 말한다. 다른 말로 SAM(Single Abstract Metehod)이라고 불리기도 한다. public interface Example { void doSomething(String string); // public abstract 생략 } 위의 인터페이스는 하나의 추상 메서드를 가지고 있는 인터페이스이다. 즉, 함수형 인터페이스의 조건을 충족하고있다. 위처럼 정의된 함수형 인터페이스는 람다를 이용해 접근할 수 있다. Example func = (strin.. 2023. 3. 29.
VARCHAR(20)에는 한글로 몇 글자까지 입력가능할까? 레벨1의 하이라이트 체스미션을 진행하면서, DB에 게임룸마다 게임을 생성하고 저장하는 기능을 구현해야 했다. 각각의 게임룸에 이름을 정할수 있으면 좋을것 같아서 room에 대한 테이블을 아래와 같이 작성했다. CREATE TABLE `room` ( `_id` int PRIMARY KEY NOT NULL AUTO_INCREMENT, `roomName` varchar(10), `currentTurn` varchar(5) default 'WHITE' )ENGINE=InnoDB DEFAULT CHARSET=utf8; table에서 CHARSET을 utf-8로 설정하였기에, 한글은 3글자까지 밖에 안들어가겠지 하고이것저것 테이블에 값을 집어넣어 봤다. 근데 예상과는 달리 한글이 4글자도 들어가고 5글자도 들어간다.. 2023. 3. 27.
어질어질 LinkedHashMap 복사하기 레벨1 - 블랙잭 미션을 진행하며, LinkedHashMap을 이용하여 플레이어와 플레이어의 배팅 금액을 저장하였었다. private final Map bettingMap = new LinkedHashMap(); LinkedHashMap을 사용한 이유는 플레이어가 생성된 순서대로 베팅금액을 입력받고, 그 순서대로 출력되기를 원했기 때문이다. 출력을 위해 당당하게 getter를 사용했는데, Map을 그냥 던져주기엔 불안하다. Map.copyOf()를 통해 복사된 Map을 던져주고 코드를 돌려봤다. 그런데 출력된 결과는 입력된 순서와는 상관없이 무작위 하게 섞여있는 것을 확인할 수 있었다. 왜죠? CopyOf() 까보기 static Map copyOf(Map 2023. 3. 13.
728x90
728x90