-
[fLOL] 01. 롤 API를 이용해 유저 정보 불러와 DB에 저장하기(1)fLOL 2020. 2. 4. 11:06
fLOL의 다른 글은 아래 더보기 버튼을 누르면 확인할 수 있습니다.
개발환경
- JAVA 8
- Spring Boot 2.1.7 + JPA
- mariaDB
주의사항
이 글을 쓰는 저는 자바에 대한 이해도가 매우 낮으며, 현재 프로그래밍을 접한지 얼마 안된 학생입니다. 따라서 이 글은 정확하지 않을 수 있으며 저 또한 공부하는 과정을 올린 것 이니 참고하여 봐주시길 바랍니다.
일단 토일 알바를 하느라 포스팅을 할 수 없었다. 하지만 1일 1커밋은 꼭 하기로 했기 때문에 알바 가기 전, 알가 끝나고 잠이 안올 때 등등...아무튼 여러번 커밋했다. 개발 환경 세팅까지만 글을 작성하고 다음 기능 추가때 포스팅을 못한게 거슬렸다.
일단 맨 처음 만든 기능은 유저 닉네임을 넣으면 그 유저의 정보를 주는 기능이었다. 솔직히 말하자면 아직 다 못만들었다. 얼추 만들긴 했는데 내가 원하는 정도와 이룬 정도는 아래와 같다.
- 닉네임을 적으면 그 유저의 고유 id, 레벨, 닉네임이 나오게 한다.
- 만약 DB에 그 유저의 닉네임이 존재한다면 Repository에서 save가 아닌 Entity에 있는 update를 실행한다.
- 닉네임을 적으면 그 유저가 최근 20경기에서 많이 한 챔프를 선정한다.
- 어제 알바 끝나고 학교에 키보드 챙기러 가면서 이 부분은 해결했다. JSONArray때문에 한 두시간은 쓴거같지만 결과물이 나쁘지 않았다.
2중 for문 있는거만 빼면...
- 그리고 최근 5경기의 승패를 W와 L로 표시해주자
- 이 부분은 좀 많이 고민했다. 일반까지 합쳐서 보여줄지
- 아니면 랭크의 5경기만 판단할지.
물론 위 이야기 말고도 개발적으로 크게 달라진게 있다. 일단 나는 Git을 멍청하게 쓰고있었다. 그저 클라우드 저장소처럼 그저 내가 만든 코드를 GitHub에 올리고 다른 작업환경(컴퓨터 내부적인 환경이 아닌 외부적인 환경이다. 물론 집-노트북-학교가 끝이지만)의 컴퓨터에 그것을 내려받아 동기화 시킨다.
GitHub를 그냥 클라우드 저장소처럼 쓰고있던거다. 브랜치도 master딱 하나를 두고 말이다. 아직 Git쪽은 잘 공부하지 않아서 잘 몰랐는데, issue기반으로 프로젝트를 관리하면 좋다고 한다.
어 아무튼 Github issues와 Projects를 사용하기 시작했다.
아마 대충 이렇게 쓰는거 아닐까? Git에 대한 내용은 추후에 따로 올리겠다.
이제 진짜 코드를 보고 어떻게 짠건지 되돌아보는 시간을 가져야 겠다.
현재 프로젝트 구성은 이렇다
음..지저분하다... - BaseTimeEntity는 생성 시간과 수정되는 시간을 자동으로 추가해주는 엔티티다.
- Champion은 단순하게 롤 챔피언들의 고유 id와 이름을 가지고 있는 엔티티다.
- User는 /add/{nickname}을 통해서 추가되는 회원들의 데이터인 엔티티이다.
- 아래 Service, Controller, DTO모두 User 관련이다.
일단 UserController부터 확인해보자.
@Controller @AllArgsConstructor public class UserController { private UserService userService; @ResponseBody @GetMapping("/add/{nickname}") public String addFriend(@PathVariable String nickname) { Long id = -1L; String msg = "생성에 실패했습니다."; try { id = userService.addUser(nickname); if(id > -1L) { msg = userService.getWelcomeString(id); } } catch(Exception e) { e.printStackTrace(); } return msg; } }
좀 지저분하게 짜긴 했다. ResponseBody로 json 형태의 데이터를 뿌려주기로 했다.
json 형태의 데이터를 뿌려준다는건 resource밑의 뷰파일로 이어지는게 아닌 말그대로 String값이 그대로 출력된다.
이런식으로 msg에 있는 값만 출력해준다.
닉네임이 혐오스러운건 내 아이디가 아니라 나도 어쩔 수 없다. 처음 @PathVariable을 접했을땐 조금 어색했다. 학교에서 프로젝트 과제를 하면서 Spring을 만졌을땐 http://주소/주소.do?파라미터=값 이런 형태의 주소만 썼기 때문이다. 저 것을 접하고 링크가 더 깔끔해진거 같아 만족스럽다.
기본적으로 Long 형태의 id값에 -1L을 할당했다. 0도 아니고 왜 -1L인가...싶을텐데 처음엔 0이 있었다.
원래 -1이면 생성실패 0이면 update 1이상이면 생성 으로 가려했었는데...update시에도 user의 id값을 가져올 수 있게되면서 자연스럽게 0만 사라졌다.
왜 Long 형태 잡았나요? 라고 물어볼 수 있다. 왜냐면 내가 모든 Entity의 id값을 모두 Long으로 설정했기 때문이다. 이는 UserService파일을 보면 왜 그랬는지 알게된다.
그리고 이제 그 id에 userSerivce의 addUser(username)이라는 메서드로 값을 받아오려한다. 이제 UserService로 가보자.
먼저 addUser 메서드부터 확인한다.
@Transactional public Long addUser(String nickname) { log.info(getClass().getName() + " start"); Long result = -1L; // result가 -1이면 생성 실패 // result가 1이상이면 업데이트, 생성 성공 nickname = nickname.replaceAll("\\p{Z}", "").toLowerCase(); UserDTO userDTO = new UserDTO(); try { log.info("cntUserByNickname(nickname) 의 값은? : " + cntUserByRawName(nickname)); if(cntUserByRawName(nickname)) { log.info("유저 데이터 있음, 갱신 요청"); User user = userRepository.findByRawName(nickname); // 요청한 닉네임을 가진 유저의 Entity를 가져옴 String accountId = user.getAccountId(); // 그 Entity에서 accountId 추출 log.info("userRepository.findByName(nickname) 로 가져온 user.accountId : " + user.getAccountId()); userDTO = getUserByAccountId(accountId); // 그 accountId로 LOL API 서버에 정보 요청 log.info("getUserByAccountId(accountId) 로 가져온 user.name : " + user.getName()); user.update(userDTO.getName(), userDTO.getRawName(), getRecentChampion(accountId), userDTO.getSummonerLevel()); // Entity의 update 메서드를 사용해 닉네임, 레벨 갱신 log.info("update 로 수정한 user.name : " + user.getName()); result = user.getId(); userDTO = null; log.info("result 값 : " + result); } else { log.info("유저 데이터 없음, 추가 요청"); userDTO = getUserByName(nickname); // 요청한 닉네임으로 LOL API 서버에 정보 요청 userDTO.setChampion(getRecentChampion(userDTO.getAccountId())); // 가장 최근 20게임 많이한 챔피온 가져오기 result = userRepository.save(userDTO.toEntity()).getId(); // DB에 저장 userDTO = null; log.info("result 값 : " + result); } } catch (Exception e) { e.printStackTrace(); log.info("회원정보 가져오기 실패!"); } return result; }
코드를 이쁘게 쓰는 재주가 없어서 많이 지저분하다. 그리고 log가 많아야 테스트때 오류 잡기가 쉬워서...좀 많이 찍은 경향이 있다.
// addUser Long result = -1L; // result가 -1이면 생성 실패 // result가 1이상이면 업데이트, 생성 성공 nickname = nickname.replaceAll("\\p{Z}", "").toLowerCase(); UserDTO userDTO = new UserDTO();
보시다시피 결과값인 result이다. 컨트롤러에도 똑같은 값이 있다. 그리고 nickname의 모든 공백을 없애고 toLowerCase로 모두 소문자로 만들었다.
이는 롤 닉네임에 공백, 대소문자 구분 이 모두 들어가기 때문이다. 처음에 이거 때문에 애 많이 먹었다.
그리고 User 엔티티에도 rawName이라는 공백 제거, 모두 소문자로 처리한 컬럼이 따로 있다.
// addUser try { log.info("cntUserByNickname(nickname) 의 값은? : " + cntUserByRawName(nickname)); if(cntUserByRawName(nickname)) {
먼저 try catch 부분의 시작이다.
cntUserByRawName의 로직 부터 살펴보겠다. 이름에서 부터 알듯이 해당 nickname이 있는지 체크하는 로직이다.
// cntUserByRawName @Transactional public Boolean cntUserByRawName(String rawName) { if(userRepository.countByRawName(rawName) > 0) { // 닉네임이 존재한다면 True return true; } // 닉네임이 존재 안하면 False return false; }
Repository에서 rawName을 검색해본다. 그리고 그 수가 0 이상이라면 존재한다고 판단해 true를 반환한다. 아니면 당연히 false를 반환한다.
왜 == 1이 아니고 > 0 이냐면 아래와 같은 경우가 있을 수 있다.
돼지찌개 라는 유저와 김치찌개 라는 유저가 있다. 여기서 돼지찌개 유저는 자신의 닉네임을 돼지찌개0026으로 변경한다.
평소 돼지찌개 닉네임이 가지고 싶었던 김치찌개 유저가 돼지찌개 닉네임으로 자신의 닉네임을 변경한다.
물론 위의 경우엔 문제가 없다. 돼지찌개는 돼지찌개0226으로 김치찌개는 돼지찌개로 닉네임을 바꾸면 되지 않는가?
그런데 만약 돼지찌개가 자신의 아이디 정보를 갱신시키지 않고 김치찌개 유저만 자신의 정보를 갱신시킨다면?
DB상에 돼지찌개란 닉네임이 2개가 되는거다.거창하게 썼는데 암튼 아이디 별로 갱신을 해야하기 때문에 중복된 닉네임이 생길 수 있다는 거다. 이제 addUser 로직으로 돌아오자!
// addUser log.info("유저 데이터 있음, 갱신 요청"); User user = userRepository.findByRawName(nickname); // 요청한 닉네임을 가진 유저의 Entity를 가져옴 String accountId = user.getAccountId(); // 그 Entity에서 accountId 추출 log.info("userRepository.findByName(nickname) 로 가져온 user.accountId : " + user.getAccountId()); userDTO = getUserByAccountId(accountId); // 그 accountId로 LOL API 서버에 정보 요청 log.info("getUserByAccountId(accountId) 로 가져온 user.name : " + user.getName());
위에서 cntUserByRawName 이 True일때 실행하는 if 문 내용이다.
이미 정보가 있기 때문에 User 엔티티 형태의 user를 userRepository.findByRawName으로 가져온다.
유저의 닉네임이 변경되었을 수도 있기 때문에 accountId라는 고유한 값을 가져온다!
accountId는 계정 아이디이다.
물론 롤 API는 이를 암호화해서 준다! 실제로 롤 API사이트에 간다면 accountId나 summonerId는 모두 encryted 되있음을 알려준다.
당연히 누구나 접근(API KEY를 받는다는 전제하에)가능한 API에 아이디를 준다는것 부터가... 다시 코드로 돌아와 이제 getUserByAccountId(accountId) 를 이용해서 accountId를 이용해서 유저의 정보를 가져온다.
롤 API에서 제공하는 유저 정보는 아래와 같다.
{ "profileIconId": 3602, "name": "귀욤미짱정훈", "puuid": "eGi-SQaJVLcSRysZAZhVrtnN-0pKWBwlXDROG-9SsukUUtsTjX1KZKZu77iEWtmEh4YfrauAu3ToyA", "summonerLevel": 145, "accountId": "1YdnoswDy8TFAeYG7DW_mb1JkLaCx8VrpSYV58ETMCo54S8", "id": "J_SWeQf5ntFxpVIzepaTtsD1DlOBt7niLMOpEdMHVOTlepc", "revisionDate": 1580611034000 }
물론 이를 DB에 차곡차곡 넣는다. 이 정보들을 불러오기 위해서 getUserByAccountId(accountId) 를 이용한다. 이제 그 메서드를 확인해보자.
// getUserByAccountId @Transactional public UserDTO getUserByAccountId(String accountId) { RestTemplate restTemplate = new RestTemplate(); HttpEntity entity = makeEntity(); URI url = URI.create("https://kr.api.riotgames.com/lol/summoner/v4/summoners/by-account/" + accountId); ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, UserDTO.class); UserDTO userDTO = (UserDTO) response.getBody(); userDTO.setRawName(userDTO.makeRawName(userDTO.getName())); return userDTO; }
일단 REST API로 정보를 불러오기 위해 RestTemplate를 사용했다. HttpEntity는 Http 헤더에 정보를 넣어 보내는 기능이다.
롤 API는 주소창에 쿼리를 써서 보내는 QueryParam(http://주소.com/주소?쿼리=값 의 형태), 그리고 나처럼 요청을 보내는 헤더에 정보를 보내는 Header Param이 있다. 난 백엔드 단에서 이를 만질것이기 때문에 굳이 Query Param을 쓰지 않고 Header Param을 채택해서 사용한다.
그리고 HttpEntity를 생성하기 위한 makeEntity()도 따로 만들었다. 이는 롤 API KEY는 거진 하루마다 만료되는데 재 발급시에 계속해서 바뀌는 키를 한번에 관리 하기 위해서다.
// makeEntity private HttpEntity makeEntity() { HttpHeaders headers = new HttpHeaders(); headers.set("Origin", "https://developer.riotgames.com"); headers.set("Accept-Charset", "application/x-www-form-urlencoded; charset=UTF-8"); headers.set("X-Riot-Token", "라이엇에서 제공하는 토큰"); headers.set("Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7"); headers.set("User-Agent", "내 브라우저 정보"); HttpEntity entity = new HttpEntity("parameters", headers); return entity; }
MakeEntity는 딱히 말할게 없다.
그렇게 만든 HttpEntity를 넣어 URI.create로 만든 URI와 함께 요청을 보낸다.
restTemplate.exchange(url, HttpMethod.GET, entity, UserDTO.class); 이 부분을 보면 UserDTO.class라고 썼다. Key가 같다면 자연스럽게 UserDTO 형태로 Value가 들어간다.
response에 UserDTO 형태의 값이 들어갔으니 이제 그것을 UserDTO에 넣어준다. 그리고 setRawName을 이용하여 rawName도 직접 넣어서 보내준다.
사실 rawName을 넣을 일이 없다면 response.getBody()를 그대로 내보내줘도 무방하지 않을까 싶다.
이제 다시 addUser로 돌아온다.
여기저기 왔다갔다 하느라 글이 정신사나운거 나도 안다...// addUser user.update(userDTO.getName(), userDTO.getRawName(), getRecentChampion(accountId), userDTO.getSummonerLevel()); // Entity의 update 메서드를 사용해 닉네임, 레벨 갱신 log.info("update 로 수정한 user.name : " + user.getName()); result = user.getId(); userDTO = null; log.info("result 값 : " + result);
그리고 user 엔티티에 있는 update문을 이용하여 닉네임, rawName, 선호하는챔피언(getRecentChampion 메서드는 추후에 설명), 현재 레벨 까지 모두 갱신 시켜준다.
그리고 result에 user의 ID 값을 넣어준다. 그리고 userDTO를 null 값으로 비워준다.
이제까지 닉네임이 이미 DB에 존재할 경우 갱신시키는 동작을 보았다.
쓰다보니 너무 중구난방이고 여기저기 왔다갔다 하다가 글이 너무 길어졌다. 아마 3부작으로 나눠야 모든걸 설명할 수 있겠다. 오전 중으로는 이 글을 쓰고 오후엔 개발을 하려했기 때문에 여기서 끊도록 하겠다.
전체 코드는 아래 Github에 가서 볼 수 있다.
https://github.com/doncolmi/fLOL
'fLOL' 카테고리의 다른 글
[fLOL] 01. 롤 API를 이용해 유저 정보 불러와 DB에 저장하기(2) (1) 2020.02.10 [fLOL] 개발 환경 세팅 (0) 2020.02.01 [fLOL] 기획 (0) 2020.02.01