그 외의 작업은 모두 AWS 에서 이루어 진다. 서브 도메인에 버켓주소를 추가해주고, 대체 도메인 주소를 설정해서 가려주었다.
AWS에서의 처리 과정 : 서브 도메인 만들고 -> 서브 도메인 이름과 버켓 이름을 같게 맞춰준다(Route 53) -> S3 버켓에서 정적 웹사이트 호스팅 활성화 시켜주기 -> Cloud Front 에서 원본이미지 (엔드포인트가 나와있는) 주소 대신 대체 도메인 이름을 설정 !
2. 느낀 점 :
시니어 개발자님의 피드백을 받을때마다 생각지도 못한 놓쳤던 부분을 많이 알게 된다. 예전엔 개발자라 하면 그저 무뚝뚝한 이미지가 떠올랐는데 이젠 엄청 세심한 이미지가 떠오른다. 코딩의 세계는 정말 어렵다 !
시그널링 서버 연결 시에 sendMessage() 메소드가 동시적으로 작동했을 때 소켓 연결이 끊어지는 이슈가 있었는데, 이를 synchronized 를 활용해서 순차적으로 진행될 수 있도록 변경.
서비스 로직 정상 작동 확인 후 해당 서비스 로직에 다른 서비스 로직을 끼워넣었을 때, 끼워 넣은 서비스 로직이 실행되는 않는 이슈 발생 → 끼워넣은 서비스 로직에서 데이터를 삭제하는 로직이 포함되어 있어서 첫 서비스 로직에서 @Transactional 을 붙여준 뒤 해결
리프레쉬 토큰 사용을 위한 Redis 데이터 베이스에서 TTL을 걸어두었지만 시간이 지나도 DB에서 삭제되지 않는 것으로 확인 → Redis에서 만료 시간을 설정하는 방법은 RedisTemplate 오퍼레이션 사용시 만료 시간을 설정하는 법과 CrudRepository를 이용하여 저장할 객체에 timeToLive를 설정하는 방법 두 가지가 있는데, RedisTemplate만을 이용하면서 객체에 timeToLive를 적용해 제대로 적용되지 않음. 제대로 적용할 수 있게 CrudRepository를 사용해 timeToLive가 적용될 수 있도록 해결
게시판 이미지 S3에 업로드 후 저장 경로가 서비스 도메인과 동일하게 시작 → SSL 인증이 안 되기 때문에 프론트에서 이미지를 보여줄 수 없음 → S3 버킷 이름을 변경하여 해결
뒤로가기 버튼이 아닌 인터넷 브라우저 탭 강제 종료 시 데이터가 변하지 않는 상황 발생 → WebRTC 연결 끊기는 로직에 게임 강제 종료 로직 추가
2. 프로젝트에 새롭게 도입한 기술 :
CI/CD 자동화 배포
github action을 통해 CI를 거친 빌드 파일이 S3에 저장된 다음 AWS CodeDeploy에 의해 EC2에 배포됩니다.
도입 이유 : 자동 테스트, 빌드, 배포를 통해 개발자가 더 편리하게 개발에만 집중 할 수 있도록 해줍니다.
동시성 제어
비관적 락을 통한 DB제어와 synchronized를 활용한 session 동시성 제어 적용
DB 제어 도입 이유: 한방에 동시 입장시 동일한 데이터에 대한 수정 요청이 발생해 데이터 일관성에 문제가 생길 것이라 생각해 이를 방지하기 위해 락을 설정했습니다.
세션 제어 도입 이유: 시그널링 서버에 여러 명이 동시에 접속 시 Websocketsession 충돌로 연결이 끊기는 이슈를 해결하기 위해 도입했습니다. session에서 sendMessage는 동시 전송이 불가능하여 이를 차례대로 처리할 수 있도록 해당 메소드에 synchronized를 적용했습니다.
3. 이번주 한 일:
CI/CD (Github Actions, AWS CodeDeploy)
방 입장하기 동시성 제어
와일드 카드 또는 제네릭 형태의 return 값 구체화
커뮤니티 게시판 API
시그널링 서버 메세지 발송 메소드 동시성 제어
S3 이미지 업로드 기능 구현
Refresh 토큰 구현 중 (FE와 맞춰봐야 함)
GameStartSet DB Redis에서 MySQL로 변경
DB 관련 로직 RepositoryService로 분류하여 리팩토링
HashMap 타입 일괄 Map으로 수정 (추후 유지보수를 위해 상위 클래스인 Map으로 수정)
GameService와 GameRearService 관심사에 맞게 통합 운영 (리팩토링)
게임 키워드 추가
추후 마케팅 작업을 위한 유저 별 방 생성 횟수, 플레이타임 등 Column값 추가 및 로직 구현
4. 궁금했던 부분:
현재 synchronized 로 동시성을 제어하는 것이 굉장히 느리다고 알고 있는데, Spring 공식 문서에서 메세지 전송을 동기화 하기 위해서 ConcurrentWebSocketSessionDecorator 도 제공하고 있다고 해서 두 가지 중에 고민입니다.
일단 웹소켓을 못가져오는 이유를 먼저 찾아보고 그래도 모르겠으면
일단 Concurr ~ 먼저 적용 , 그래도 안되면 Synchronized 사용 .
최대한 싱크로나이즈드는 지양하는게 좋다. 성능이 너무 안좋음.
Controller 단에서 ResponseEntity<?>와 같이 반환 데이터 타입을 명시적으로 정해주지 않고 사용했었는데, Front로 내보내는 반환 값으로 와일드카드를 쓴다는 건 반환하는 메소드에서 의도한 타입으로 나가든, 의도하지 않은 타입으로 나가든 성공적으로 나가게 되는거 같습니다. 내보내는 반환 값을 명확하게 설정해주면 잘못된 타입으로 나가는 걸 방지해줄 수 있기 때문에 명시적으로 사용하는 거 같은데 옳게 이해한 걸까요? -
옵션 1. 하이브리드 방식
? extends Object - 이런식으로 쓸 수 있다. 특정 클래스의 하위클래스만 내보내는 방법이기도 하다
옵션 2. ? 그대로 사용 - 문제는 없음.
현재 메세지 브로커를 STOMP 내장 브로커를 활용하고 있는데, 스케일아웃을 했을 때 동작하지 않는 것으로 알고있습니다. 만약의 상황에서 서버를 스케일아웃을 했을 때 정상작동하도록 메세지 브로커로 Redis나 RabbitMQ를 사용하는 것이 나을까요?
현재 메시지브로커 역할을 하는지? 멘토님이 알기론 아님.
브로커 입장에서는 이미 바라보고 있는 데이터가 스케일 아웃을 하던 안하던 상관없다.
인스턴스 객체가 인메모리상에 뜨는데 얘가 다른 인스턴스의 스톰프를 알아야되는 상황이 맞는지 ?
토요일은 기술 멘토링이 있는 날이다. 현직에 계시는 시니어님께 웹소켓 세션이 끊어지는 이슈에 대해 여쭤보고 synchronized 키워드로 막아놨다고 말씀드렸더니 잘못된 방식일 것 같다는 피드백을 주셨다. 일단 먼저 왜 웹소켓이 끊어지는이 이유를 찾아야하고 synchronized 키워드 사용은 최대한 지양해야 한다고 말씀해 주셨다. 어렵다.
2. 느낀 점 :
현직 시니어님의 피드백을 받을때마다 CS지식이 정말 많이 부족함을 느낀다. 좀 더 잘 이해하고 더 구글링을 잘 하기 위해선 기본 지식이 필요한데, 지금까지 항해99 과정을 따라오면서 실무적인 부분, 과제구현에 급급하느라 많은 부분의 공부를 놓치고 있지 않았나 라는 생각이 들었다. 대학에서 4년을 배워도 부족하다고 할텐데 이 많은 지식을 3개월 - 4개월 만에 흡수하려니 쉽지가 않다.
3. 새로 알게 된 내용:
synchronized키워드 사용은 최대한 지양해야 한다.
interface와 class 의 차이를 확실히 알아야 한다. ( Map은 interface 이다. )
Stomp는 메세지브로커가 아니다. 이 역할에 대해 제대로 공부해놓기.
4. 셀프칭찬 (오늘 잘한 일):
지난 크리스마스도, 새해도, 설날도 그리고 오늘 내 생일도 항해99를 하며 보냈다. 이 시기가 지나고 나면 개발자가 되지 못하더라도 후회는 없을 것 같다. 마지막까지 후회 없이 잘 버티고 열심히 공부하자 !
팀원분의 지인이 카카오 현직 개발자로 계셔서 우리 깃헙 코드를 보고 아래와 같은 피드백을 주셨다. 그래서 오늘은 이부분들을 하나씩 해결하는 빡쎈 하루였다.
업적 기능이 과연 기술적으로 꼭 들어가야하는 기능인지
Optional.get() 안티패턴이니 이렇게 쓸거면 사용하지 말 것
GameRoom에서 status가 왜 스트링인지, -> boolean 타입으로 바꿀 것
HashMap -> Map으로 수정, 항상 상위타입으로 쓰는 습관 들일 것
이미지 업로드 동작 방식을 아는지
ResponseEntity<?> 쓰지 말것, 항상 타입은 명확하게 -> 이유는 찾아보라고 함
정적 타입의 장점 찾아볼것, 타입 안정성도
throws IOException 무작정 던지지 말고 예외처리 하고 던져야됨
Stomp 동작 방식 명확하게 이해하고 있어야됨, 웹소켓이랑 연관이 없는 기능이다
메세지 브로커는 왜 내장 브로커 사용했는지, 왜 쓰면 안 되는지
LettuceConnectionFactory 뭔지 공부
아마존 S3 경로 아마존 주소 안 나오게 수정해야됨
로그인 기능 컨트롤러 -> 서비스 -> 컨트롤러로 되어 있는데 컨트롤러까지 안 가고 Spring Security단에서 끝낼 수 있음
그리고 오늘 세션이 랜덤하게 끊어지는 부분을 synchronized키워드를 사용하여 스레드 간 동기화를 시켜주는 방법으로 막아보았다. 일단 돌아가는 것 같기는 한데 정답이 맞는지는 몰라서 이번주 토요일 피드백 시간에 시니어분께 여쭤보려 한다.
2. 느낀 점 :
갈수록 어렵다 !!!!
3. 새로 알게 된 내용:
팀원분들께 내가 맡은 Refresh Token 관련 코드리뷰를 해드려야 하는데, 코드 대부분을 그냥 냅다 긁어다 썼다보니 Jwt, Spring Security 에 관한 이해가 너무 없어서 한줄 한줄 메소드가 어떤 역할을 하는지 정리하는 시간을 가져보았다. 오늘 공부한 부분 링크 참조 !
4. 셀프칭찬 (오늘 잘한 일):
새로 알게된 부분은 최대한 블로그에 적으려고 했다. 다른 팀원분이 나에게 매일 블로그를 기록하는 것이 대단하다고 칭찬해 주셨다. 그래 내가 내세울 수 있는 건 꾸준함인 것 같다.
@Getter
@Setter
public class RefreshToken implements Serializable {
private static final long serialVersionUID = 6494678977089006639L;
private String refreshToken;
private String email;
public RefreshToken(String token, String email) {
this.refreshToken = token;
this.email = email;
}
public RefreshToken updateToken(String token) {
this.refreshToken = token;
return this;
}
}
redis에 저장되는 객체들은 Serialize 가능해야함
이 값을 지정해주지 않으면 컴파일러가 계산한 값이 부여됨(변동성있음) 게다가 컴파일러는 Serializable class 혹은 Outer Class를 참고하여 만들기 때문에 이 클래스가 변동이 되면
serialVersionUID도 변경이 있을 수 있음 (역시 변동성이 있음)
이 UID가 달라지면 기존에 저장된 객체를 읽을 수가 없게 됨. (저장하는 쪽, 불러오는 쪽 컴파일러가 다를 경우, 저장하는 시기의 클래스 내용과 불러오는 시기의 클래스의 내용이 다를 경우 등)
데이터를 저장하는데 있어 이런 변동성은 위험하기에 serialVersionUID을 지정.
출처 : 소영님
아래는 3조 분들이 노션에 작성한 부분 ! ( 출처 : 눈치코치캐치 노션)
ChatRoom 에 Serializable 를 구체화한 이유 Redis의 저장방식은 byte array 형식이기 때문에 직렬화를 하여 캐싱을 할 때 넘겨줘야 합니다. Java에서의 객체 직렬화는 Serializable 인터페이스를 구현한 것에서만 가능하다고 이해를 하고 있습니다. 그래서 Redis를 이용하는 ChatRoom 에서 Serializable 를 구체화 했습니다.
2. TokenDto
@Getter
@NoArgsConstructor
public class TokenDto {
private String accessToken;
private String refreshToken;
public TokenDto(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
3. RefreshTokenRepository
// 기능 : Redis에 Refresh Token 저장
@Repository
@RequiredArgsConstructor
public class RefreshTokenRepository {
private final RedisTemplate<String, String> redisTemplate;
private long expiredTime = 3 * 60L; // 리프레시토큰 유효시간 : 3분
// 특정 RefreshToken 조회
public RefreshToken findByEmail(String email){
RefreshToken refreshToken = new RefreshToken(redisTemplate.opsForValue().get(email), email);
return refreshToken;
}
// RefreshToken 저장
public RefreshToken saveRefreshToken(RefreshToken refreshToken){
redisTemplate.opsForValue().set(refreshToken.getEmail(), refreshToken.getRefreshToken(),expiredTime,TimeUnit.SECONDS); // 리프레시 토큰 유효시간 설정 : 3분 이부분 추후에 수정 필요 !
return refreshToken;
}
// RefreshToken 삭제
public void deleteRefreshToken(String email){
redisTemplate.delete(email);
}
}
1. findByEmail()
redisTemplate.opsForValue() 를 이용해 Redis에 String 형태의 리프레시토큰을 저장하고, 사용자를 식별해 줄 이메일을 저장해준다.
2. saveRefreshToken()
리프레시 토큰을 레디스와 저장함과 동시에 유효시간을 저장해준다. 유효시간이 끝나면 자동 삭제 된다.
테스트용이기 때문에 일단 exprited time을 3분으로 설정해둠. 실제 서비스에는 1주 - 2주 정도로 할 예정.
RedisTemplate은 Redis 서버에 Redis 커맨드를 수행하기 위한 high-level-abstractions을 제공하고 Object 직렬화, Connection management를 수행한다.
또한 Redis 서버에 데이터 CRUD를 위해, Redis의 다섯가지 데이터 유형에 대한 Operation Interface를 제공하는데 아래와 같다.
< 메소드명반환 오퍼레이션Redis 자료구조 >
opsForValue()
ValueOperations
String
opsForList()
ListOperations
List
opsForSet()
SetOperations
Set
opsForZSet()
ZSetOperations
Sorted Set
opsForHash()
HashOperations
Hash
RedisTemplate: RedisTemplate은 Thread-Safe 하며 재사용 가능
JacksonJsonSerializer: JSON 포맷으로 데이터를 저장하는 경우
※ Redis 내장 함수를 통해 저장되어있는 데이터를 유효시간이 지나면 자동 삭제할 수 있는데, 두 가지 방법이 있다. 지금 작성되어있는 방법이 아니라 다른 방법으로 수정할 예정이라 수정이 끝나는 대로 비교해서 다시 포스팅 하는 걸로 ! 투비컨티뉴 ....! 수정이 끝나는 대로 깃헙 링크를 올리겠습니다 : )