1. 백엔드는 더 분발해야 한다. 너무 스코프가 작음    인프라 부분 강화하기

  • API명세 : Swagger 사용
  • 통합배포 : 깃헙 액션 사용
  • 배포 자동화 :  ( 깃헙 액션 ) CI/CD 사용
  • 무중단 배포 : Docker 사용

 

2. 개인 피드백 

  • 리프레시 토큰 관련/ 인증관련해서 더 공부하기 ( 무조건 면접 질문으로 나옴 )
  • 백에서 관리할지 프론트로 보낼지 연구해보기 

 

3. 지난 금요일 https 배포후 발생했던 오류에 대한 질문 

  • 로컬환경이랑 배포환경이 다르기때문에 오류가 날 수 있음 도커를 사용해야하는 이유
  • SSL 설정을 바꾼다거나 해야할 듯. ( 적용에 문제가 있을 듯 )

 

4. 웹소켓 세션을 서버 인메모리에 저장하고 있는데, 어떤게 좋은 방법인지에 대한 질문에 대한 답변

  • 지금 처럼 사용해도 되지만 서비스가 커져서 서버를 여러개 사용할 경우 한계가 있음
  • 네트워크 기반 스토리지에 돌리는게 맞다. 레디스에 저장하는 걸로 바꾸는게 좋음
  • 로컬캐시는 실제로 많이 사용하지만 성능적으로 보완해야 할 부분이 많다.

 

5. 스프링 3대 요소는 면접 단골 질문

  1. IOC/ DI
  2. AOP
  3. PSA
  4. Solid 객체지향 설계방법 

 

6. 추가 피드백 

  • Repository - service 단에 사용하는 쿼리를 분리해야한다.
  • 서비스단에서 너무 많이 DB를 조회하는 것은 좋지 않음. 특히 이 부분이 중복될 경우 따로 클래스를 만들어 분리하는게 좋음.

 

우리팀 화잇팅 !

 

1. 어려웠던 부분 : 오늘도 프론트엔드에 값을 넘겨줄 때 자잘한 에러가 있었다. 공지사항에 오답일 경우 "닉네임"님의 정답은 오답이라고 뜨는 부분이 있는데 여기서 닉네임 부분에 null값이 들어갔다. 백엔드 코드에서 이 닉네임을 Member Repository에서 받아오고 있었는데, 게임 로직을 수정하면서 이젠 Member 엔티티가 필요한게 아니라 프론트에서 넘겨주는 값을 받아야 했다. 

이 부분을 수정하기 위해 GameDto 라는 디티오를 만들고 (RequestDto 가 되겠다.) 프론트에서 게임을 하면서 넘겨주는 닉네임값을 받아주었다. 그리고 여기서 받은 닉네임을 아래의 사진과 같이 필요한 부분에 붙여주니 정상 작동 되었다.

 

 

2. 느낀 점 : 지난주 멘토님의 피드백을 반영해 도커를 시도해 보려했다. 참 개발은 공부의 끝이 없는 것 같다. 예전에 어떤 멘토분께서 개발은 파면 밑도 끝도 없으니 그때 그때 필요한 정보를 필요한 만큼만 알아내는 것도 능력이라고 하셨는데 무슨 의미인지 알 것 같다. 개발을 시작하기 전엔 전혀 몰랐던 분야를 접하고 보니, 세상엔 참 내가 모르는 것들이 많다는 사실이 새삼 다시 한번 느껴졌다. 아직 고작 3개월 차 코린이 인데 모르는게 더 많은게 당연한 거니까 조급해 하지말고, 천천히 단단하게 배워나가야 겠다.

 

3. 새로 알게 된 내용 : 3조에서 코드윗미 라는 인텔리제이회사에서 만든 서비스를 이용하는 것을 보았다. 서로 실시간으로 코드 수정을 볼 수 있고, 누가 어떤 부분을 수정하는지 알 수 있는 서비스이다. 개발이라는 분야를 시작한지 이제 3개월 정도 되었는데, 참 다양한 툴이 많고 협업하기 좋은 환경인 것 같다. 그리고 오늘 처음으로 도커라는 것을 공부했다. 생활코딩 강의와 몇개의 블로그 글들으 보면서 따라해 봤는데, 일단 따라는 했는데, 사용해 보지 않아서 정확히 어떤 점이 도커의 장점인지 어떻게 사용하는 건지는 와닿지 않았다. 일단 오늘은 세팅한 것에 의의를 ... 

 

4. 셀프칭찬 (오늘 잘한 일) : 오늘 프론트엔드에서 null값이 뜨는 오류가 있다고 했을때 너무 자연스럽게 왜 그러지 알 것 같았다. 아 맞다 그부분 수정했어야 했는데! 라는 생각이 들었다. 예전 같았으면 왜지? 라는 생각만 들었을 텐데 그만큼 코드를 보는 눈이 조금은 생긴 것 같달까? 느리지만 조금씩 성장하고 있는 나를 칭찬한다. 

 

5. 내일 할 일 : 아직 우리팀에서 해결하지 못한 CI/CD + 웹소켓 세션 객체를 레디스에 저장하는 법 같이 에러 찾아보기

 


[오늘 공부한 부분] 

  • 코드 에러 수정
  • log 공부
  • 도커 공부

[11] Docker (1)

 

[11] Docker (1)

Docker란 ? Docker는 애플리케이션을 신속하게 구축, 테스트 및 배포할 수 있는 소프트웨어 플랫폼 Docker는 소프트웨어를 컨테이너라는 표준화된 유닛으로 패키징하며, 이 컨테이너에는 라이브러리,

leejincha.tistory.com

[13] 트러블 슈팅 : HTTPS 배포 유의사항

 

[13] 트러블 슈팅 : HTTPS 배포 유의사항

문제 상황 생각보다 WebRTC 시그널링 연결이 오래 걸렸는데, http인 로컬 서버로만 했을 때는 문제없던 기능들이 https 서버로 환경을 바꿔주었더니 틈만나면 아래와 같은 에러 혹은 403, 404, 405에러,

leejincha.tistory.com

[52] Spring boot Logging(@Slf4j)

 

[52] Spring boot Logging(@Slf4j)

프로젝트를 진행하면서 에러가 발생할 경우 데이터가 잘 들어가는지 확인하기위해 로깅을 할 때가 자주 있다. 오늘은 스프링부트 lombok에서 제공하는 (@Slf4j)를 이용한 로깅법을 정리해보려 한다

leejincha.tistory.com

 

프로젝트를 진행하면서 에러가 발생할 경우 데이터가 잘 들어가는지 확인하기위해 로깅을 할 때가 자주 있다. 오늘은 스프링부트 lombok에서 제공하는 (@Slf4j)를 이용한 로깅법을 정리해보려 한다.

 

실제로 이번 프로젝트에 사용한 예제

 

Logging 이란? 

  • 로깅(logging)은 정보를 제공하는 일련의 기록인 로그(log)를 생성하도록 시스템을 작성하는 활동을 말한다.
  • 프린트 줄 넣기(printlning)는 간단한, 보통은 일시적인, 로그를 생성하기만 한다. 
  • 시스템 설계자들은 시스템의 복잡성 때문에 로그를 이해하고 사용해야 한다.
  • 로그가 제공하는 정보의 양은, 이상적으로는 프로그램이 실행되는 중에도, 설정 가능해야한다.

 

Logging 사용시 장점

  • 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.
  • 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있다.
  • 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있다. 특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.
  • 성능도 일반 System.out보다 좋다. (내부 버퍼링, 멀티 쓰레드 등등) 그래서 실무에서는 꼭 로그를 사용해야 한다.

 

로그 선언 및 호출

 

//선언
private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class)

//lombok
@Slf4j

//호출
log.info("hello");

 

간단한 동작 예제

package hello.springmvc.basic;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

//http body 에 string 을 넣어줌
// 기존 Controller 와 차이 있음
@RestController
@Slf4j //private... Logger 선언 귀찮을 때 대체할 수 있음
public class LogTestController {

//    private final Logger log = LoggerFactory.getLogger(getClass());

    @RequestMapping("/log-test")
    public String logTest(){
        String name = "Spring";

        //앞으로 이런방식 권장하지 않음
        System.out.println("name = " + name);

        log.trace(" trace my log="+ name);

        log.trace("trace log={}", name);

        log.debug("debug log={}", name);
        log.info(" info log={}", name);
        log.warn(" warn log={}", name);
        log.error("error log={}", name);
        return "ok";
    }
}
  • LEVEL : TRACE > DEBUG > INFO > WARN > ERROR
  • 개발 서버는 debug 출력
  • 운영 서버는 info 출력

올바른 사용법

  • log.debug("data="+data");
    • 로깅레벨이 debug 보다 높을 때에도 문자 더하기 연산이 수행됨(출력이 안되어도)
  • log.debug("data = {},data);
    • 의미없는 연산 발생하지 않음

 

 


[ 참고 자료 ]

 

문제 상황

  • 생각보다 WebRTC 시그널링 연결이 오래 걸렸는데, http인 로컬 서버로만 했을 때는 문제없던 기능들이 https 서버로 환경을 바꿔주었더니 틈만나면 아래와 같은 에러 혹은 403, 404, 405에러, Error parsing HTTP request header 에러를 비롯한 아래와 같은 다양한 에러들을 뱉어냈다. 

 

에러 1
에러 2

 

원인

  • 현재 프론트엔드는 AWS S3로 배포를 했고, 백엔드는 AWS EC2로 각각 서버를 배포한 상태 
  • 배포한 주소는 다르지만 AWS에서 같은 인증서를 사용했다.
  • 그런데, 인증서에 백엔드 도메인만 등록하고 프론트 도메인 주소를 등록해 주지 않아서 발생한 오류!
  • 즉, 프론트엔드 도메인은 https 반영이 안되어 있고 백엔드 도메인만 https 반영이 되어 있어서 충돌이 났던 것 같다.(추측)

 

해결 방법

  • 사용하는 인증서에 백엔드 주소(api.namoldak.com), 프론트엔드 주소(namoldak.com) 모두 등록해 주었다.
  • AWS > Route 53 > 호스팅 영역 > 레코드

 

 

  • AWS Certificate Manager > 인증서

 

 

 

  • 아래에 레코드 생성을 클릭한 다음 프론트엔드 도메인 주소와 백엔드 도메인 주소 모두 사용하는 인증서에 등록해 주었더니 https와 관련된 모든 에러가 해결되었다 ! 

 

 

GameController

  • 웹소켓을 사용하기 때문에 @PostMapping 이 아닌 @MessageMapping 을 사용한다.
  • @PathVariable 대신 @DestinationVariable 을 사용한다.
@Slf4j
@RequiredArgsConstructor
@RestController
public class GameController {

    private final GameService gameService;

    // 게임 시작
    @MessageMapping("/game/{gameRoomId}/start")
    public ResponseEntity<?> gameStart(@DestinationVariable Long gameRoomId,
                                       GameDto gameDto) {
        gameService.gameStart(gameRoomId, gameDto);
        return ResponseUtil.response(StatusCode.GAME_START);
    }

 

KeywordRepository

  • SELECT * FROM table ORDER BY RAND(); 는 DB에서 랜덤으로 값을 추출해오는 쿼리문이다. 이 쿼리문을 이용하여 아래와 같이 작성하였다. limit 숫자 를 붙임으로써 숫자만큼의 데이터 혹은 원하는 데이터 만큼의 레코드를 가져올 수 있다.
  • @Query("select * from keyword(테이블 이름) k where k.category(테이블 안의 Colum) = :category order by rand() limit 4(4개 불러오기)")
  • 그리고 findTop4ByCategory 라는 JPA 쿼리를 통해 지정된 카테고리를 갖고있는 키워드가 4개만 조회되도록 했다. 
  • List<Keyword> findTop4ByCategory(@Param("category") String category);
public interface KeywordRepository extends JpaRepository<Keyword, Long> {
    @Query(value = "select * from keyword k where k.category = :category order by rand() limit 4", nativeQuery = true)
    List<Keyword> findTop4ByCategory(@Param("category") String category);

    @Query(value = "select * from keyword k where k.category = :category order by rand() limit 3", nativeQuery = true)
    List<Keyword> findTop3ByCategory(@Param("category") String category);
}

 

GameService ( 게임 시작 API )

1. 수정 전 코드

  • 수정 전 작성한 코드는 키워드 엔티티 안에 카테고리가 있고, 카테고리 엔티티는 따로 없는 상태로 구현한 로직이다.
  • 구현한 로직의 순서는 다음과 같다.
    • 카테고리 엔티티가 없기 때문에 바로 카테고리 조회가 불가능 했다. 그래서 가장 먼저 키워드 하나를 랜덤으로 조회해서 그 해당 키워드 카테고리를 가져온다. 랜덤 조회는 Math.random() 메소드를 이용했다.
    • 그 다음 방금 가져온 카테고리를 가진 모든 키워드들을 List<Keyword>에 담아 주었다.
    • 참여인원에 따라 keywordRepository에서 findTop4ByCategory(category) / findTop3ByCategory(category) 를 이용하여 랜덤으로 4개, 3개의 키워드를 가져온다.
    • 그다음 키워드와 게임 참여 인원을 키:밸류 로 맺어줄 HashMap<> keywordToMember 을 하나 만들어 준다. 
    • 게임참여 인원의 목록이 담긴 List<> memberListNickname 을 만들어 준다.
    • for문을 돌면서 랜덤으로 가져오 키워드를 게임 참여한 멤버에 배당한다.
    • 게임 정보를 담아주는 gameStartSet리포지토리에 반영된 데이터를 저장해 준다.
        // 멤버들에게 뿌려지게 될 키워드 전체 목록 불러오기
        List<Keyword> keywordList1 = keywordRepository.findAll();

        // 랜덤으로 키워드 하나 뽑기
        Keyword keyword1 = keywordList1.get((int) (Math.random() * keywordList1.size()) + 1);

        // 위에서 랜덤으로 뽑은 키워드의 카테고리
        String category = keyword1.getCategory();

        // 같은 카테고리를 가진 키워드 리스트 만들기
        List<Keyword> keywordList;

        if (gameRoomAttendees.size() == 4) {
            // 참여 멤버가 4명 이라면, 랜덤으로 키워드 4장이 담긴 리스트를 만들어 준다.
            keywordList = keywordRepository.findTop4ByCategory(category);
        } else if (gameRoomAttendees.size() == 3) {
            // 참여 멤버가 3명 이라면, 랜덤으로 키워드 3장이 담긴 리스트를 만들어 준다.
            keywordList = keywordRepository.findTop3ByCategory(category);
        } else {
            throw new CustomException(NOT_ENOUGH_MEMBER);
        }

        HashMap<String, String> keywordToMember = new HashMap<>();

        // 웹소켓으로 방에 참가한 인원 리스트 전달을 위한 리스트
        // 닉네임만 필요하기에 닉네임만 담음
        List<String> memberNicknameList = new ArrayList<>();

        for (GameRoomAttendee gameRoomAttendee : gameRoomAttendees) {
            memberNicknameList.add(gameRoomAttendee.getMemberNickname());
        }

        //게임룸 멤버한테 키워드 배당
        for (int i = 0; i < gameRoomAttendees.size(); i++) {
            keywordToMember.put(memberNicknameList.get(i), keywordList.get(i).getWord());
        }

        GameStartSet gameStartSet = GameStartSet.builder()
                .roomId(gameRoomId)
                .category(category)
                .keywordToMember(keywordToMember)
                .round(0)
                .spotNum(0)
                .winner("")
                .build();

        // StartSet 저장
        gameStartSetRepository.save(gameStartSet);

 

2. 수정 후 코드

  • 위에 작성된 코드는 모든 DB를 조회하는 로직이라 좀 더 효율적인 코드를 위해 Category라는 Enum 클래스를 만들어 주었다.
  • 아래는 변경된 서비스 부분이다.
  • 키워드 전체 목록을 가져와서 랜덤으로 하나 뽑아주는 로직을 제거하였다.
  • Enum 클래스 안에 만들어 놓은 .getRandom().name() 메소드를 이용하여 Enum으로 저장되어 있는 카테고리 중 하나를 랜덤으로 가져오게 하였다.

	// 카테고리 랜덤으로 가져오기 (변경된 부분)
        String category = Category.getRandom().name();

        // 같은 카테고리를 가진 키워드 리스트 만들기
        List<Keyword> keywordList;

        if (gameRoomAttendees.size() == 4) {
            // 참여 멤버가 4명 이라면, 랜덤으로 키워드 4장이 담긴 리스트를 만들어 준다.
            keywordList = keywordRepository.findTop4ByCategory(category);
        } else if (gameRoomAttendees.size() == 3) {
            // 참여 멤버가 3명 이라면, 랜덤으로 키워드 3장이 담긴 리스트를 만들어 준다.
            keywordList = keywordRepository.findTop3ByCategory(category);
        } else {
            throw new CustomException(NOT_ENOUGH_MEMBER);
        }

 

Category

  • getRandom() 메소드를 이용해 랜덤으로 값을 가져온다.
@Getter
public enum Category {

    인물, 동물, 음식, 만화, 영화, 악기;

    public static Category getRandom(){
        return values()[(int)(Math.random()* values().length)];
    }
}

 


[ 참고 자료 ]

 

 

+ Recent posts