LostCatBox

SpringProject-TrackingPost-CH02

Word count: 1.3kReading time: 8 min
2022/12/24 Share

Project 통합택배조회 api 02편 (첫번째 구현)

Created Time: August 31, 2022 10:57 AM
Last Edited Time: September 16, 2022 2:43 PM
Tags: Java, Spring, Computer

왜?

첫번째 구현은 api서비스를 만들 것이며, MSA, kafka고려하여 서비스별 관계 설정후 구현할 것이다.

???

  • jsoup은 js가 처리안되어나오고, webclient는 tag들에 값이 들어가있는걸보면 된느거같은데??

  • 추후 timeout설정도 반드시 해주기(상대 서버가 죽었을가능성있음)

  • 응답을 제대로 못받았을경우 어떻게 대처할지 처리하기

  • msa구성할때, service별로 분리한것이 중요한 개념

    → msa마다 db 가 없어도 상관없음.

  • message가 바뀌면 저장시도, message같다면 필요없이 해당 로우에 updateDate 를 변경 → message로 하면안될듯… message라면 바뀔가능성 있음. 아예 없을수도있음. 해당 택배회사의 택배 업데이트 날짜가 바뀌면 업데이트하는것이 나아보임

시퀀스 다이어그램

Untitled

참고

대표적인 외부 택배조회 API의 JSON값

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
{
"parcelResultMap": {
"resultList": [
{
"invcNo": "364321198184",
"sendrNm": "양**",
"qty": "1",
"itemNm": "가전제품류 [300000] 기타상품 37",
"rcvrNm": "김**",
"rgmailNo": "",
"oriTrspbillnum": "",
"rtnTrspbillnum": "",
"nsDlvNm": "42"
}
],
"paramInvcNo": "364321198184"
},
"parcelDetailResultMap": {
"resultList": [
{
"nsDlvNm": "",
"crgNm": "보내시는 고객님으로부터 상품을 인수받았습니다",
"crgSt": "11",
"dTime": "2022-08-27 17:22:14.0",
"empImgNm": "EMP_IMG_NM",
"regBranId": "7247",
"regBranNm": "서울삼전베스트",
"scanNm": "집화처리"
},
{
"nsDlvNm": "",
"crgNm": "물류터미널로 상품이 이동중입니다.",
"crgSt": "41",
"dTime": "2022-08-29 15:58:17.0",
"empImgNm": "EMP_IMG_NM",
"regBranId": "V168",
"regBranNm": "송파A",
"scanNm": "간선상차"
},
{
"nsDlvNm": "",
"crgNm": "배송지역으로 상품이 이동중입니다.",
"crgSt": "44",
"dTime": "2022-08-29 21:22:40.0",
"empImgNm": "EMP_IMG_NM",
"regBranId": "V001",
"regBranNm": "곤지암Hub",
"scanNm": "간선상차"
},
{
"nsDlvNm": "",
"crgNm": "고객님의 상품이 배송지에 도착하였습니다.(배송예정:이강욱 010-6886-7181)",
"crgSt": "42",
"dTime": "2022-08-30 09:49:29.0",
"empImgNm": "EMP_IMG_NM",
"regBranId": "V510",
"regBranNm": "대구동",
"scanNm": "간선하차"
}
],
"paramInvcNo": "364321198184"
}
}

설계

DB 설계

RDB사용이유 및 연관관계 없는이유

  • RDB를 사용하여 DATA의 일관성 확보(택배 조회시스템상 모두 비슷한 정보가짐)
  • RDB강점인 관계는 설정 안함(이 서비스는 ID로만 거의 찾기 때문에 연관관계 이점없다고판단→ 한 테이블내에 모든 정보 찾을수있도록함→ ID의 카디널리티 높게해야함→ search 속도 높아야함(삭제기능없고, 추가만있으니 방향성확실))

아키텍처 설계

  • 내 프로젝트 위치

2

  • 내 프로젝트내 서비스별 아키택처

3

UML

  • 해보기 하지만 먼저 구현해보는게 좋아보임
  • 나중에 작성하기

Post Data field

  • (PK) Id: ID 요청
  • kakao_id: user 정보
  • sender: 보내는이
  • receiver: 받는 이
  • message: detail한 메세지
  • location: Hub → Hub
  • status_data: 택배사의 기록 업데이트 날짜 (이것을 기준으로 새로운 데이터 생성할지 말지 판단해도될듯)
  • updated_time : 내 DB에 업데이트된시간

외부 택배사 api 뜯기

대한 통운

request조건

같은 시기에 발급된 세션값+csrf값을 넣어주면 모든 요청에서 가능했음(나머지 인자 필요없음)

  • 쿠키
    • sessionid값
1
JSESSIONID=68DC345A45962C2AEA58861B86E2E913.front12; Path=/; HttpOnly;
  • body
    • csrf값
    • paramInvcNo 값 (=운동장번호값)
1
_csrf=e1030947-f0f8-48a8-8def-d7eea2e6e8d3&paramInvcNo=364321198184

csrf공격 방지 동작중…
csrf토큰역할: 요청폼이 올바른 폼인지 아니면 다른곳에서 만들어진 폼인지 구분
csrf공격때문에 생긴 방법으로 세션으로 인증되어있을때, 다른 사이트로 접속하게되면, 정보를 수정해서 타인이 보낼수있다. 이를 방지하기 위해csrf를 각 세션별로 가지고있고, 이를 사용자의 요청에 hidden으로 숨겨놓고 csrf까지 인증해야 정상적인 요청으로본다.

CUpost

cu는 iframe을 사용하고있었다.

request조건

  • get parameter
    • pTdNor값(=운송장번호)

DB

@EnableJpaAuditing

jpa사용시

@EntityListeners(AuditingEntityListener.class)

@createddate 사용시

application.properties

1
2
3
4
5
6
7
# DB ??
spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:mysql://localhost:3306/tracking_post_db?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=Test2802!
# mysql 사용 명시
spring.jpa.database=mysql

Controller별 구현

  • postDBservice관련부분은 추후 msa에 따라 빠질것

처음 요청하는 uri

  • postDBservice관련부분은 추후 msa에 따라 빠질것
1
2
3
4
5
6
7
8
//카카오톡으로 처음 요청한 경우 링크를 반환함!
@GetMapping (value = "/")
public String gethomepage(){
RequestInfo requestInfo = validRequest.getinfo(); // 요청에 대한 정보 추출
PostDto postDto = postManager.getpost(requestInfo); //해당 요청에 대해 post 정보 추출
postDbService.savePost(postDto); //dbtest, 추후 다른 msa로 빠질것임 -> 중복검사후 저장혹은 modifieddate 바꿈
return "localhost:8080"+"/"+postDto.getKakaoId()+"/"+postDto.getPostNumber()+"/";
}

내 서버에서 반환한 uri

1
2
3
4
5
6
7
// 내 서버가 응답한 link경로로 접근한경우
// id값은 DB아이디 값이며, kakaoid값으로 추후 바뀔수있다. 하지만 현재로는 이렇게 검증하고, 가장 최근 자료를 보여주는것이 좋은듯
@GetMapping ("/{userId}/{postNumber}/")
public PostDto getpostpage(@PathVariable String userId,@PathVariable String postNumber){
PostDto recentPost = postDbService.recentPost(userId, postNumber);//인자 &조건으로 가장최근 데이터 반환, 없다면 빈데이터
return recentPost;
}

새로고침할때 uri

  • 이를 post로 구현하였다. → 필요한 이유는 새로고침을 따로 만들고 쓰기 때문에, 다른기능과 통합되어 자동을 일어나는 불필요한 새로고침 요청을 축소시키는 역할을 한다.
1
2
3
4
5
6
7
8
// 새로고침 만들기 post->인자 가져와서 이미 모든 필드값같다면 modified_data만 바꿔주기
@PostMapping (value = "/")
public HttpStatus posthomepage(){
RequestInfo requestInfo = validRequest.getinfo(); // 요청에 대한 정보 추출
PostDto postDto = postManager.getpost(requestInfo); //해당 요청에 대해 post 정보 추출
postDbService.savePost(postDto); //dbtest, 추후 다른 msa로 빠질것임 ->
return HttpStatus.OK;
}

service

ValidRequest

  • 요청에서 데이터 받아서 정보 추출하는 역할 하는클래스
  • 현재 카카오톡과 연동되지않아서, 일단 이렇게 구현했다.
1
2
3
4
5
6
7
8
9
10
11
@Service
public class ValidRequest { //추후에 데이터 받아서 정보 추출하는 역할 하는클래스
public RequestInfo getinfo(){
RequestInfo requestInfo = new RequestInfo();
requestInfo.setKindRequest(KindRequest.Kakao);
requestInfo.setPostCompany(PostCompanyEnum.CJ);
requestInfo.setPostNumber("364321198184");
requestInfo.setRequestUser("test2802");
return requestInfo;
}
}

PostManager

  • postmanager는 외부 택배 api에 대해 지원하는지 판단후, 응답을 받고 이를 PostDto로 반환하는 책임을 진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
public class PostManager {

private final List<PostProvider> providerList;
@Autowired
public PostManager(List<PostProvider> providerList) { // bean에서 해당 타입으로 등록된 빈 list로 주입
this.providerList = providerList;
}

public PostDto getpost(RequestInfo requestInfo){
for (PostProvider provider:providerList){
if (!provider.isSupport(requestInfo.getPostCompany())) { //support확인후 아니면패스
continue;
}
else{
PostDto result = provider.get(requestInfo.getPostNumber());
if (result!=null){ //result 네트워크 오류시 null반환함
result.setKakaoId(requestInfo.getRequestUser());
return result;
}
else{
return new PostDto(); //null대신 에러를 대체할만한 객체필요
}
}
} //for 문을 다돌아도없다?
return new PostDto(); //null대신 에러를 대체할만한 객체필요
}
}

PostDbService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
@RequiredArgsConstructor
public class PostDbService {
private final PostRepository postRepository;

//save
@Transactional
public PostDto savePost(PostDto postDto){
Optional<Post> recentPost = postRepository.findTopByPostNumberAndKakaoIdOrderByModifiedDateDesc(postDto.getPostNumber(), postDto.getKakaoId());
if (ObjectUtils.isEmpty(recentPost)) {
postRepository.save(postDto.toEntity());
}
else {
if (recentPost.get().getStatusData().equals(postDto.getStatusData())){
recentPost.get().update(LocalDateTime.now());
}
else{
postRepository.save(postDto.toEntity());
}
}

return postDto;
}
//송장번호중 가장 최근 post정보 가져옴
public PostDto recentPost(String kakaoId, String postNumber){
Optional<Post> recentPost = postRepository.findTopByPostNumberAndKakaoIdOrderByModifiedDateDesc(postNumber,kakaoId);
if (ObjectUtils.isEmpty(recentPost)){
return new PostDto(); //조회경력없음. 빈 post
}
else{
return new PostDto(recentPost.get()); //최근데이터 하나만 반환함
}
}
}

Provider들

  • 이는 외부 택배 조회 service들을 각자 빈 등록해서 사용하였다.

문제 해결

Jsoup json 에러

문제점

org.jsoup.UnsupportedMimeTypeException: Unhandled content type. Must be text/*, application/xml, or application/xhtml+

애석하게도, jsoup은 html parser로 만들어졌고, 따라서 json content-type을 support하지않는다.

해결책

.ignoreContentType(true)를 통해 강제로 일단 파싱하여, json 파싱이가능한 ObjectMapper()를 통해 jsoup의 document에서 body().text()반환받은후 파싱을 진행하였다.

1
2
3
4
5
6
7
8
9
10
11
public PostDto get(String postNumber) {
ObjectMapper mapper = new ObjectMapper();
Map<String, String> resultMap = new HashMap<>();

JsonNode readTree = mapper.readTree(documentpost.body().text());

JsonNode node = readTree.findPath("parcelResultMap").findPath("resultList").get(0);
resultMap.put("postNumber", node.findPath("invcNo").asText());
resultMap.put("sender", node.findPath("sendrNm").asText());
resultMap.put("receiver", node.findPath("rcvrNm").asText());
resultMap.put("contentType", node.findPath("itemNm").asText());

조회 빈이 2개이상일때 문제

문제점

provider의 인터페이스의 구현체인 택배 크롤링 provider들은 ProviderManager 에서 각자 주입되어야했다. 하지만 이는 DI의 원칙을 위배한것이므로 다음과 같이 해결했다

해결

  • List 해당 타입의 모든 스프링빈 반환 특징을 이용하여 주입하였다.
1
2
3
4
5
6
7
8
9
10
@Service
public class PostManager {

private final List<PostProvider> providerList;
@Autowired
public PostManager(List<PostProvider> providerList) { // bean에서 해당 타입으로 등록된 빈 list로 주입
this.providerList = providerList;
}
...
}

추가 정보

  • @Autowired를 하면 스프링 빈에 타입을 조회로 하여 주입된다. 따라서, 같은 타입이 두개라면 문제가 발생한다.
  • 해결책은 많다.
    • @Autowired 필드 명 매칭
    • @Qualifier @Qualifier끼리 매칭 빈 이름 매칭
    • @Primary 사용
    • Map<String, DiscountPolicy> 해당 타입의 모든 스프링 빈과 해당 값 반환 → 없다면 빈 List반환
    • List 해당 타입의 모든 스프링빈 반환 →없다면 빈 List반환
CATALOG
  1. 1. Project 통합택배조회 api 02편 (첫번째 구현)
  2. 2. 왜?
  3. 3. ???
  4. 4. 시퀀스 다이어그램
  5. 5. 참고
    1. 5.1. 대표적인 외부 택배조회 API의 JSON값
  6. 6. 설계
    1. 6.1. DB 설계
      1. 6.1.1. RDB사용이유 및 연관관계 없는이유
    2. 6.2. 아키텍처 설계
    3. 6.3. UML
    4. 6.4. Post Data field
  7. 7. 외부 택배사 api 뜯기
    1. 7.1. 대한 통운
      1. 7.1.1. request조건
    2. 7.2. CUpost
      1. 7.2.1. request조건
  8. 8. DB
  9. 9. Controller별 구현
    1. 9.1. 처음 요청하는 uri
    2. 9.2. 내 서버에서 반환한 uri
    3. 9.3. 새로고침할때 uri
  10. 10. service
    1. 10.1. ValidRequest
    2. 10.2. PostManager
    3. 10.3. PostDbService
    4. 10.4. Provider들
  11. 11. 문제 해결
    1. 11.1. Jsoup json 에러
      1. 11.1.1. 문제점
      2. 11.1.2. 해결책
    2. 11.2. 조회 빈이 2개이상일때 문제
      1. 11.2.1. 문제점
      2. 11.2.2. 해결
      3. 11.2.3. 추가 정보