Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

코드 리뷰~ #4

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
build/
out/
.gradle
.idea

src/main/resources/application-db.yml
src/main/resources/application.properties
src/main/resources/application.yml
18 changes: 18 additions & 0 deletions src/main/java/kr/where/backend/BackendApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kr.where.backend;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@EnableRetry
@SpringBootApplication
public class BackendApplication {

public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}

}
116 changes: 116 additions & 0 deletions src/main/java/kr/where/backend/api/HaneApiService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package kr.where.backend.api;

import java.util.ArrayList;
import java.util.List;
import kr.where.backend.api.exception.RequestException;
import kr.where.backend.api.http.HttpHeader;
import kr.where.backend.api.http.HttpResponse;
import kr.where.backend.api.http.Uri;
import kr.where.backend.api.http.UriBuilder;
import kr.where.backend.api.json.hane.Hane;
import kr.where.backend.api.json.hane.HaneRequestDto;
import kr.where.backend.api.json.hane.HaneResponseDto;
import kr.where.backend.group.entity.Group;
import kr.where.backend.group.entity.GroupMember;
import kr.where.backend.member.Member;
import kr.where.backend.member.MemberRepository;
import kr.where.backend.member.exception.MemberException.NoMemberException;
import kr.where.backend.oauthtoken.OAuthTokenService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class HaneApiService {
private final OAuthTokenService oauthTokenService;
private final MemberRepository memberRepository;
private static final String HANE_TOKEN = "hane";

/**
* hane api 호출하여 in, out state 반환
*/
public Hane getHaneInfo(final String name, final String token) {
try {
return JsonMapper.mapping(HttpResponse.getMethod(HttpHeader.requestHaneInfo(token), UriBuilder.hane(name)),
Hane.class);
} catch (final RequestException exception) {
log.warn("[hane] {} : {}", name, exception.toString());
return new Hane();
}
}

@Transactional
public void updateInClusterForMainPage(final Member member) {
if (member.isAgree()) {
member.setInCluster(getHaneInfo(member.getIntraName(), oauthTokenService.findAccessToken(HANE_TOKEN)));

log.info("[scheduling] : {}의 imacLocation이 변경되었습니다", member.getIntraName());
}
}

public List<HaneResponseDto> getHaneListInfo(final List<HaneRequestDto> haneRequestDto, final String token) {
try {
return JsonMapper.mappings(HttpResponse.postMethod(HttpHeader.requestHaneListInfo(haneRequestDto, token),
UriBuilder.hane(Uri.HANE_INFO_LIST.getValue())), HaneResponseDto[].class);
} catch (final RequestException exception) {
log.warn("[hane] : {}", exception.toString());
return new ArrayList<>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빈 배열을 리턴하는 이유는 단순히 반환값을 맞추려고 일까요??
상위메서드에서 호출하면 빈 배열이니 밖에서는 에러가 안뜨는게 목적이였을까요??

아니면 자바 문법상 반환값이 있는 메서드들은 catch에서도 리턴을 꼭 해야하나요???

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것은 의도된것입니다.
자바 문법상 catch 했을 경우, throw를 하거나, return을 하거나 자유롭게 할 수 있습니다.
하지만 이 method는 외부 API 요청을 하네에게 하는 것이기에 우리 서비스 단의 에러라고 판단하지 않습니다.
그래서 하네측에서 에러가 날 경우, 빈 문자열을 반환하는 것입니다 그래서 log도 warn으로 설정합니다.

}
}

@Transactional
public void updateMemberInOrOutState(final Member member, final String state) {
member.setInCluster(Hane.create(state));
}

@Transactional
public void updateMyOwnMemberState(final List<GroupMember> friends) {
log.info("[hane] : inCluster 업데이트 스케줄링을 시작합니다!");
final List<HaneResponseDto> responses = getHaneListInfo(
friends
.stream()
.filter(m -> m.getMember().isPossibleToUpdateInCluster())
.map(m -> new HaneRequestDto(m.getMember().getIntraName()))
.toList(),
oauthTokenService.findAccessToken(HANE_TOKEN));

responses.stream()
.filter(response -> response.getInoutState() != null)
.forEach(response -> {
this.updateMemberInOrOutState(
memberRepository.findByIntraName(response.getLogin())
.orElseThrow(NoMemberException::new),
response.getInoutState());
log.info("[hane] : {}의 inCluster가 변경되었습니다", response.getLogin());
});
log.info("[hane] : inCluster 업데이트 스케줄링을 끝냅니다!");
}

@Transactional
public void updateGroupMemberState(final Group group) {
log.info("[hane] : 메인 페이지 새로고침으로 인한 inCluster 업데이트를 시작합니다!");
final List<HaneResponseDto> responses = getHaneListInfo(
group
.getGroupMembers()
.stream()
.filter(m -> m.getMember().isPossibleToUpdateInCluster())
.map(m -> new HaneRequestDto(m.getMember().getIntraName()))
.toList(),
oauthTokenService.findAccessToken(HANE_TOKEN)
);
responses.stream()
.filter(response -> response.getInoutState() != null)
.forEach(response -> {
updateMemberInOrOutState(
memberRepository.findByIntraName(response.getLogin())
.orElseThrow(NoMemberException::new),
response.getInoutState());
log.info("[hane] : {}의 inCluster가 변경되었습니다", response.getLogin());
});
log.info("[hane] : 메인 페이지 새로고침으로 인한 inCluster 업데이트를 끝냅니다!");
}
}
110 changes: 110 additions & 0 deletions src/main/java/kr/where/backend/api/IntraApiService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package kr.where.backend.api;

import java.util.List;
import kr.where.backend.api.exception.RequestException.TooManyRequestException;
import kr.where.backend.api.http.HttpHeader;
import kr.where.backend.api.http.HttpResponse;
import kr.where.backend.api.http.UriBuilder;
import kr.where.backend.api.json.CadetPrivacy;
import kr.where.backend.api.json.Cluster;
import kr.where.backend.exception.CustomException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class IntraApiService {

private static final String END_DELIMITER = "z";

/**
* 특정 카텟의 정보 반환
*/
@Retryable(retryFor = TooManyRequestException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retryable 어노테이션을 사용하고 3번의 재시도(maxAttempts)를 하고도 문제가 있으면 어떻게 작동하나요??
TooManyRequestException예외가 던져지는게 맞을까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3번의 재시도에도 문제가 발생한거면 우리 서비스의 문제보다는
외부 API 서비스의 exception이라고 생각하여 던집니다.
처리 방식이 생각난다면 적용해도 좋을거같아요
하지만 이 에러는 우리 자체에서 발생하는 건 아니여서 저렇게 처리했습니다.

public CadetPrivacy getCadetPrivacy(final String token, final String name) {
return JsonMapper
.mapping(
HttpResponse.getMethod(HttpHeader.request42Info(token), UriBuilder.cadetInfo(name)),
CadetPrivacy.class);
}

/**
* index page 별로 image 반환
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

index page가 정확히 무엇을 의미하는지 궁금합니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

42API는 pagable의 request를 지원하지 않아서 직접 우리가 page를 설정하여, 요청해야합니다.
즉, 1000명의 유저의 url를 받아오는 과정에서 한번에 1000명을 요청하기 보다, 페이지를 나누어서 100명씩 10번에 나눠서 요청합니다.
왜냐하면 42API는 1시간에 요청 수가 정해져 있기 때문입니다. APi Setting에서 설정 값을 확인해 보면 될거같아요

*/
@Retryable(retryFor = TooManyRequestException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public List<CadetPrivacy> getCadetsImage(final String token, final int page) {
return JsonMapper
.mappings(
HttpResponse.getMethod(HttpHeader.request42Info(token), UriBuilder.image(page)),
CadetPrivacy[].class);
}

/**
* 클러스터 아이맥에 로그인 한 카뎃 index page 별로 반환
*/
@Retryable(retryFor = TooManyRequestException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public List<Cluster> getCadetsInCluster(final String token, final int page) {
return JsonMapper
.mappings(
HttpResponse.getMethod(HttpHeader.request42Info(token), UriBuilder.loginCadet(page)),
Cluster[].class);
}

/**
* 5분 이내 클러스터 아이맥에 로그아웃 한 카뎃 index page 별로 반환
*/
@Retryable(retryFor = TooManyRequestException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public List<Cluster> getLogoutCadetsLocation(final String token, final int page) {
return JsonMapper
.mappings(HttpResponse.getMethod(HttpHeader.request42Info(token),
UriBuilder.loginBeforeFiveMinute(page, false)), Cluster[].class);
}

/**
* 5분 이내 클러스터 아이맥에 로그인 한 카뎃 index page 별로 반환
*/
@Retryable(retryFor = TooManyRequestException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public List<Cluster> getLoginCadetsLocation(final String token, final int page) {
return JsonMapper
.mappings(HttpResponse.getMethod(HttpHeader.request42Info(token),
UriBuilder.loginBeforeFiveMinute(page, true)), Cluster[].class);
}

/**
* keyWord 부터 end 까지 intra id를 가진 카뎃 10명의 정보 반환
*/
@Retryable(retryFor = TooManyRequestException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public List<CadetPrivacy> getCadetsInRange(final String token, final String keyWord, final int page) {
return JsonMapper
.mappings(HttpResponse.getMethod(
HttpHeader.request42Info(token),
UriBuilder.searchCadets(keyWord, keyWord + END_DELIMITER, page)),
CadetPrivacy[].class);
}

/**
* 요청 3번 실패 시 실행되는 메서드
*/
@Recover
public CadetPrivacy fallbackCadetPrivacy(final CustomException exception) {
log.warn("[IntraApiService] CadetPrivacy method");
throw exception;
}

@Recover
public List<CadetPrivacy> fallbackCadetsPrivacy(final CustomException exception) {
log.warn("[IntraApiService] List<CadetPrivacy> method");
throw exception;
}

@Recover
public List<Cluster> fallbackClusterList(final CustomException exception) {
log.warn("[IntraApiService] List<Cluster> method");
throw exception;
}
}
38 changes: 38 additions & 0 deletions src/main/java/kr/where/backend/api/JsonMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package kr.where.backend.api;


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.List;

import kr.where.backend.api.exception.JsonException;

public class JsonMapper {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 ObjectMapper의 개체가 대문자와 스네이크케이스로 작성된 이유가 뭔가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자바 컨벤션은 상수로 정의할 경우 스네이크 케이스를 사용합니다!


public static <T> T mapping(final String jsonBody, final Class<T> classType) {
try {
return OBJECT_MAPPER.readValue(jsonBody, classType);
} catch (JsonProcessingException e) {
throw new JsonException.DeserializeException();
}
}

public static <T> List<T> mappings(final String jsonBody, final Class<T[]> classType) {
try {
return Arrays.asList(OBJECT_MAPPER.readValue(jsonBody, classType));
} catch (JsonProcessingException e) {
System.out.println(e.getMessage());
throw new JsonException.DeserializeException();
}
}

public static String convertJsonForm(final List<?> requestBody) {
try {
return OBJECT_MAPPER.writeValueAsString(requestBody);
} catch(JsonProcessingException e) {
throw new JsonException.DeserializeException();
}
}
}
50 changes: 50 additions & 0 deletions src/main/java/kr/where/backend/api/TokenApiService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package kr.where.backend.api;

import kr.where.backend.api.exception.RequestException.TooManyRequestException;
import kr.where.backend.api.http.HttpHeader;
import kr.where.backend.api.http.HttpResponse;
import kr.where.backend.api.http.UriBuilder;
import kr.where.backend.api.json.OAuthTokenDto;
import kr.where.backend.exception.CustomException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class TokenApiService {

/**
* intra 에 oAuth token 발급 요청 후 토큰을 반환
*/
@Retryable(retryFor = TooManyRequestException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public OAuthTokenDto getOAuthToken(final String code) {
return JsonMapper
.mapping(HttpResponse.postMethod(HttpHeader.requestToken(code), UriBuilder.token()),
OAuthTokenDto.class);
}

/**
* refreshToken 으로 intra 에 oAuth token 발급 요청 후 토큰 반환
*/
@Retryable(retryFor = TooManyRequestException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public OAuthTokenDto getOAuthTokenWithRefreshToken(final String refreshToken) {
return JsonMapper
.mapping(HttpResponse.postMethod(
HttpHeader.requestAccessToken(refreshToken), UriBuilder.token()),
OAuthTokenDto.class);
}

/**
* 요청 3번 실패 시 실행되는 메서드
*/
@Recover
public OAuthTokenDto fallback(final CustomException exception) {
log.info("[TokenApiService] back token 발급에 실패하였습니다");
throw exception;
}
}
14 changes: 14 additions & 0 deletions src/main/java/kr/where/backend/api/exception/JsonErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package kr.where.backend.api.exception;

import kr.where.backend.exception.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum JsonErrorCode implements ErrorCode {
DESERIALIZE_FAIL(1200, "json 맵핑에 실패 했습니다.");

private final int errorCode;
private final String errorMessage;
}
15 changes: 15 additions & 0 deletions src/main/java/kr/where/backend/api/exception/JsonException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.where.backend.api.exception;

import kr.where.backend.exception.CustomException;

public class JsonException extends CustomException {
public JsonException(final JsonErrorCode jsonErrorCode) {
super(jsonErrorCode);
}

public static class DeserializeException extends JsonException {
public DeserializeException() {
super(JsonErrorCode.DESERIALIZE_FAIL);
}
}
}
17 changes: 17 additions & 0 deletions src/main/java/kr/where/backend/api/exception/RequestErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kr.where.backend.api.exception;

import kr.where.backend.exception.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum RequestErrorCode implements ErrorCode {
UNAUTHORIZED(3000, "Unauthorized 권한이 없습니다."),
TOO_MANY_REQUEST(3001, "42API 요청 횟수를 초과하였습니다."),
SERVER_ERROR(3002, "외부 API 서버 에러입니다."),
BAD_REQUEST(3003, "잘못된 요청입니다");

private final int errorCode;
private final String errorMessage;
}
Loading