@WebMvcTest를 진행시 Header에 Authorization을 할당 할 경우 아래 처럼 SecurityRequirements는 다양한 형태로 생길 수 있다.

근데 똑같이 헤더 이름을 지정하고, 테스트용 토큰 문자열을 할당하는데, 어떻게 아래처럼 다양한 결과를 맞을 수 있을까?

 

일단 스웨거 UI에서 우측 상단 아래에 있는 버튼을 활성화하려면 JWT_BEARER이어야 한다.

그 외 OAUTH2 관련 스펙을 정의하는건 별도 설정을 통해서 가능한데,

우선 나는 활성화를 하고 싶은데 안되어서,

securityRequirements의 type을 결정하는건 대체 누가 해주나 싶어서 찾아 보았다.

 

결과는 아래와 같다.

 

토큰의 페이로드에 오는 데이터의 형태에 따라 결정된다고 보면 된다.

scope가 포함된 토큰을 사용시에는 OAUTH2로 형태가 지정되고,

그게 아닌 경우 JWT_BEARER,

토큰은 있으나, 형태가 불분명하면, null로 처리되더라.

jwt 토큰 검증 관련 테스트는 별개로 하고,

mvcTest의 테스트 코드에 jwt처리하는 부분을 모킹하고 아무 토큰이나 주워다 넣었는데,

하필 그게 OAUTH2 토큰이었고,

별거 아닌곳에서 헤매게 되었지만, 나름 기능상 중요한 사실을 알게된 시간.

OCP와 DIP를 지키기 위해
인터페이스와 구현체를 분리하고 다형성을 갖게끔 하는게 좋은 코드란건 알겠고,
결국 인터페이스를 상속받고 필요한 구현체만 구현하면 된다는걸 다들 알겠다고 한다.
 
근데 정작 사용을 어떻게 하는지 대부분 모르는 것 같다.
 
 
일단 개인적으로 그다지 좋지 않은 코드의 예시다.

    public interface PayService {

        void pay();
    }

    private class PayKakaoService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PaySamsungService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PayNaverService implements PayService {

        @Override
        public void pay() {

        }
    }

    private PayService payKakaoService;
    private PayService paySamsungService;
    private PayService payNaverService;

    @Getter
    @RequiredArgsConstructor
    public enum PayType {
        Kakao, Samsung, Naver;
    }

    @PostMapping("/payment")
    public ResponseEntity<?> pay(PayType payType) {

        if (payType == PayType.Kakao) {
            payKakaoService.pay();
        }

        if (payType == PayType.Samsung) {
            paySamsungService.pay();
        }

        if (payType == PayType.Naver) {
            payNaverService.pay();
        }

        return ResponseEntity.ok().build();
    }

    // 실행
    void callPayTest() {
        this.pay(PayType.Samsung);
    }

위 코드의 단점이 뭘까? 분명 인터페이스도 썻고, 필요에 따라 구현체도 만들었고,
근데 저기에 앱쁠페이를 추가한다면? 구현체를 만들지만

public ResponseEntity<?> pay(PayType payType)

요기 안에 있는 if문을 추가해야 하지 않겠는가?!
 
이거는 DIP에 위배 되는게 아닌가? 싶기도 하고, 

PayService ps = null;

if (payType == PayType.Kakao) {
    ps = payKakaoService;
}

if (payType == PayType.Samsung) {
    ps = paySamsungService;
}

if (payType == PayType.Naver) {
    ps = payNaverService;
}

ps.pay();

아님 뭐 메서드 내 필드를 이용해서 이렇게 바꿀까? 이것도 뭐 크게 다른건 없다고 느끼는데,
그렇다고 싱글톤 기반의 빈에서 클래스 필드를 맘대로 교체할 수도 없는 노릇이고,
 
좀 더 고차원 레이어에 영향이 안가는 방법의 코드를 작성 할 수는 없을까?
 
있다, Enum과 Provider를 사용하면 된다.
Spring Container는 등록된 모든 Bean에 대한 제어가 가능하다.
일단 등록되어 있으니 가져오는 것도 가능하다.
구조적으로는 위에 if문을 사용하는 것과 차이가 없지만,
 
적어도 인터페이스를 호출 하는 부분을 타입이나, 유형 때문에 바꿀 일은 없을 거다.
 
예시 코드 들어간다.
 
참고로 제 코드가 좋은 코드는 아닙니다용..제발...
 

    public interface PayService {

        void pay();
    }

    private class PayKakaoService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PaySamsungService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PayNaverService implements PayService {

        @Override
        public void pay() {

        }
    }

//    private PayService payKakaoService;
//    private PayService paySamsungService;
//    private PayService payNaverService;

    @Getter
    @RequiredArgsConstructor
    public enum PayType {
        Kakao(PayKakaoService.class), Samsung(PaySamsungService.class), Naver(
            PayNaverService.class);

        private final Class<? extends PayService> clazz;
    }

    public class PayServiceProvider {

        private static ApplicationContext applicationContext;
        private static Map<PayType, Class<? extends PayService>> payServiceMap = new HashMap<>();

        static {
            for (PayType payType : PayType.values()) {
                payServiceMap.put(payType, payType.getClazz());
            }
        }

        public static PayService service(PayType payType) throws Exception {
            Class<? extends PayService> payServiceClass = payServiceMap.get(payType);

            if (payServiceClass == null) {
                throw new Exception();
            }

            return applicationContext.getBean(payServiceClass);
        }
    }

    @PostMapping("/payment")
    public ResponseEntity<?> pay(PayType payType) throws Exception {

//        PayService ps = null;
//
//        if (payType == PayType.Kakao) {
//            ps = payKakaoService;
//        }
//
//        if (payType == PayType.Samsung) {
//            ps = paySamsungService;
//        }
//
//        if (payType == PayType.Naver) {
//            ps = payNaverService;
//        }
//
//        ps.pay();

        PayServiceProvider.service(payType).pay();

        return ResponseEntity.ok().build();
    }

    // 실행
    void callPayTest() throws Exception {
        this.pay(PayType.Samsung);
    }

일단, 해당 코드는 provider에서 사용하는 applicationContext에 대한 주입이 없긴하다.
요건 이제 @PostConstruct를 사용해서 주입하면 되긴합니다요.
물론 주입하는 함수도 provider에 static으로 만들어 줘야 합니다.
(대충 읽고 코드만 복붙하면 안되게 하려는 함정...)
 
대강 저런식이면, 앞으로 앱쁠페이든, 티머니든 추가되면 Enum과 구현체의 추가만으로 솔리드 뭐시기를 위배하지 않으면서 해결가능하지 않을까?
 
사실 Provider 구현하기 귀찮다.
유지 보수 측면에서는 좋긴하나, 처음 구현할때 걍 if else 쓰는게 백배 편하다.
시간 비용을 초기에 쓸거냐 나중에 쓸거냐 차이긴한데,
 
아래는 좀 더 편한 방법

private List<PayService> payServices;

@PostMapping("/payment")
public ResponseEntity<?> pay(PayType payType) throws Exception {

    payServices.stream().filter(payService -> payService.check(payType)).findFirst()
        .ifPresent(PayService::pay);
    return ResponseEntity.ok().build();
}

 
스프링은 주입시 배열로 필드를 선언하면 해당 인터페이스에 해당하는 빈들을 모두 가져 오는데요, 이걸 몰랐던건 아닌데, 이렇게 응용할 생각은 못 했네요. 멍청이...
 
아는분이 알려주신건데 아래 링크 통해 배우셨대요!
https://youtu.be/3MTf43_RcVM

 

linux 에서 톰캣 환경 설정시 별 다른 생각 없이 하다보면 헤매는 부분 중 하나.

 

3월즘에도 엄청 헤맸는데, 이번에도 하루는 날려버린 부분...ㅠ

 

톰캣실행이 실행은 아주 잘 되는데 스프링 부트는 deploy가 제대로 되지 않는다.

 

이유는 os에 있는 기본 openjdk로 실행되기 때문,

 

설정문제인가해서 설정파일을 엄청 찾아보지만~~ 찾을 수 없고

 

실행로그를 확인하다보니 jdk 1.8.xx 라고 써있는걸 발견

 

잉?난 zulu-11 설치했는뎅?

 

> sudo update-alternatives --config java

 

위 명령어로 디폴트로 사용할 자바 버전을 선택해 줄 수 있다.

 

다음에 또 헤맬까봐 글 남기는중...ㅠ

 

구글링해도 안나와서 엄청 헤매다가 해결하려니 스트레스 ㅠ

오라클 자바 삭제 후 openjdk 를 설치해야하는데,

openjdk에는 cert파일도 내용이 없죠.ㅠㅠ

https request하면 인증서 관련 오류를 마구 뱉어내요.


그래서 윈도우 사용하는 로컬머신에 일단 테스트 후 azul사의 zulu를 사용하기로 결정.


okky에서도 반응이 좋은듯 해서 이걸로...ㅎㅎ


참고로 msi를 제공하기에 윈도우에서는 기존 자바 폴더와 똑같이 설정해주면, 환경변수나 기타 설정을 할 필요가 없어요.


https://www.azul.com/files/zulu_8.9_install.pdf


해당 메뉴얼 보시면 아주 잘 나와 있습니다만, 영어 싫어 하시는분들을 위해서 4줄 요약


> sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 0x219BD9C9

> sudo apt-add-repository 'deb http://repos.azulsystems.com/ubuntu stable main'

> apt-get update

> apt-get install zulu-8



그리고 자바홈 설정


> vi /etc/profile

...

export JAVA_HOME=/usr/lib/jvm/zulu-8-amd64

...


오라클자바 삭제는


> apt-get remove oracle*

> apt-get autoremove --purge

> apt-get autoclean


오라클 자바 삭제는 아래 글 참고했어용.

http://rudas5594.tistory.com/271


아래 처럼 나온다면 아주 성공적!


> java -version

openjdk version "1.8.0_181"

OpenJDK Runtime Environment (Zulu 8.31.0.1-linux64) (build 1.8.0_181-b02)

OpenJDK 64-Bit Server VM (Zulu 8.31.0.1-linux64) (build 25.181-b02, mixed mode)

인터넷 찾아가며 이것저것 짜집기도 하고 영 별거 아닌걸로 고생아닌고생하니까 짜증이 나네요...


그래서 제가 짠거 공개할게요.

springdml restTemplate 이용해서 했고요..

인터넷 찾아보니, 이게 뭐 convert가 좀 문제라서...

한글이 마구 깨지는데..

이거 구글링 열심히해서 어떤분 소스 베껴다가 넣었어요.

원본 주소 : https://gist.github.com/ucpwang/949145408a12bb40a671


주석같은거 안달려 있으니 알아서 보시면 될거 같고요.

contents에서 img태그 찾아서 #n 으로 치환해서 멀티파트로 파일 넘기는것까지 구현했습니다.

error코드 인증실패 리턴시에는 토큰값 갱신 하시는 로직 따로 작성하시면 되어요.



public String blogPost(String token, String title, String contents) { contents = contents.replaceAll("<br>", ""); contents = contents.replaceAll("<BR>", ""); List<String> imageSrcs = new ArrayList<>(); List<Resource> resources = new ArrayList<>(); int startIdx = 0; int imgIdx = contents.indexOf("<img", startIdx); while(imgIdx > -1){ int srcIdx = contents.indexOf("//image", imgIdx); int endIdx = contents.indexOf("\"", srcIdx); String imgSrc = contents.substring(srcIdx, endIdx); imageSrcs.add(imgSrc); startIdx = endIdx; imgIdx = contents.indexOf("<img", startIdx); } int idx = 0; for(String imageSrc : imageSrcs){ contents = contents.replace(imageSrc, "#"+idx); idx++; int qIdx = imageSrc.indexOf("?"); if(qIdx < 0){ qIdx = imageSrc.length(); } imageSrc = imageSrc.substring(0, qIdx); String fileSrc = imageSrc.replace(imageServerHost, saveDirectory); Resource file = new FileSystemResource(fileSrc); resources.add(file); } String header = "Bearer " + token; // Bearer 다음에 공백 추가 RestTemplate restTemplate = new RestTemplate(); String apiURL = "https://openapi.naver.com/blog/writePost.json"; try { for (HttpMessageConverter<?> hmc : restTemplate.getMessageConverters()) { if (hmc instanceof AllEncompassingFormHttpMessageConverter) { /** AllEncompassingFormHttpMessageConverter 생성자 내용 일부 가져와서 수정 **/ List<HttpMessageConverter<?>> partConverterList = new ArrayList<HttpMessageConverter<?>>(); partConverterList.add(new ByteArrayHttpMessageConverter()); StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("UTF-8")); stringHttpMessageConverter.setWriteAcceptCharset(false); partConverterList.add(stringHttpMessageConverter); partConverterList.add(new ResourceHttpMessageConverter()); partConverterList.add(new SourceHttpMessageConverter()); if (ClassUtils.isPresent("javax.xml.bind.Binder", AllEncompassingFormHttpMessageConverter.class.getClassLoader())) { partConverterList.add(new Jaxb2RootElementHttpMessageConverter()); } if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AllEncompassingFormHttpMessageConverter.class.getClassLoader())) { partConverterList.add(new MappingJackson2HttpMessageConverter()); } else if (ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader()) && ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", AllEncompassingFormHttpMessageConverter.class.getClassLoader())) { partConverterList.add(new MappingJackson2HttpMessageConverter()); } ((AllEncompassingFormHttpMessageConverter) hmc).setPartConverters(partConverterList); } } HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", header); MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<String, Object>(); parameters.add("title", title); parameters.add("contents", contents); for(Resource resource : resources){ parameters.add("image", resource); } HttpEntity<?> httpEntity = new HttpEntity<>(parameters,headers); ResponseEntity<HashMap> re = restTemplate.exchange(apiURL, HttpMethod.POST, httpEntity, HashMap.class); System.out.println(re); return (String) re.getBody().get("error_code"); } catch (Exception e) { System.out.println(e); } return null; }


+ Recent posts