728x90

들어가며

개인 프로젝트에서 유닛 테스트를 짜고 있었다. SMS, 이메일 인증 로직을 테스트 하려고 하다가 고민에 빠졌다. 해당 로직은 외부 캐시 서비스를 사용 해서 인증번호 대조를 진행한다. 이를 테스트 하기 위한 방법론에 대한 고민에 빠진 것이다. 고민은 Spy 객체를 만들어서 코드로 로직을 검증할까, 아니면 실제 데이터를 가지고 검증을 할까 고민이었다. 해당 서비스는 외부 서비스를 의존하는 서비스라서 자바 코드로 검증하기엔 단지 flow만 검증하는 느낌이었다. 그래서 flow 뿐만 아니라 데이터도 검증 해서 더 정확히 검증 할 수 있게 실제로 Redis를 붙여서 검증하기로 결정 하였다. Mockist인 나로서는 고민이 되는 부분이었다.

 

해결

애플리케이션을 전부 올리는 SpringBootTest를 사용하기엔 테스트 시간을 너무 소비하는 느낌이어서 생산성이 떨어질 것 같았다. 그래서 test profile 전용 application yaml 파일을 만들고 테스트에 쓰일 Redis config 파일을 만들었다. 그 다음으로 Redis를 실행 시켜야 하는데 배포 전에 CI 가 실행 되는 github actions VM 에 매번 이러한 테스트가 생길 때마다 해당 하는 이미지를 올리는 등 이러한 관리 공수가 들것이라고 생각했다. 매번 CI script 코드도 수정 해야 하는데, 테스트 코드와 script 싱크를 맞추는 것도 추후 서비스가 더 커지면 귀찮은 일이라고 느껴질 수 있겠다고 생각했다. 그래서 이것도 자동화 하고싶었다. 그래서 github actions VM에 도커 환경을 세팅 해준 후 ./gradlew build 을 해주면 자동으로 @Testcontainer가 붙은 클래스에서 필요한 이미지를 세팅 해주는 테스트 환경을 구성했다.

 

기대할 수 있는 점

Testcontainer를 활용하면 앞으로 다양하게 테스트를 운영 환경이랑 최대한 비슷하게 해볼 수 있을거라고 기대한다. 지금까지 integration test는 H2 in-memory 디비로 했었지만 한계가 있었다. 호환하지 않은 문법 등 많아서 테스트를 위해 관리 해야 할 점들이 많았었다. 그런점에서 운영 환경이랑 비슷하다고 생각이 안되었다. (그렇다고 해서 도커를 띄워서 운영 환경을 만들 수 없다는건 아니긴 하다. 만들 수는 있지만 억지로 만들어주는 느낌으로 운영 코드랑 필요한 이미지를 싱크를 CI script로 맞춰 줘야 하는 단점이 있다.) 그러나 Testcontainer는 필요한 이미지를 코드단에서 관리가 가능하기 때문에 테스트에 더 집중할 수 있다고 생각한다. 그리고 코드로 운영 환경을 최대한 구성할 수 있어서 테스트를 실제 운영환경이랑 비슷하게 할 수 있다는 장점이 있었다.

728x90

배경

회원가입 유저 인증 방식이 sms, email 두 가지가 있다. 기존에는 각각의 Handler에서 EmailUtilService, SmsUtilService 를 has-a 관계로 가지고 있었다. 처음 이런 클래스 구조에 대해 의문을 갖게 된 계기는 이런 관계는 테스트하기에 용이하지 않았다는 점이었다. 변경 전 구조에서 mock 테스트를 하려면 세부적인 부분(캐시 데이터 같은 것들)을 검증하지 못했다.

 

 

해결 방법 모색

  1. 각 UtilService에 인터페이스를 implements 하기 이렇게 하면 테스트에만 용이한 구조이고 다른 부분에서는 이점을 찾아볼 수 없었다. 관리 해야 할 클래스(대표적으로 인터페이스), 패키지만 늘어나고 테스트 용이성만 가져가는 구조로 판단 된다.
  2. 하나의 인터페이스 클래스만 두고 각 UtilService 들이 implements 하기 이 구조는 클래스는 안늘어나지만 유연하지 않은 구조였다. 지금은 각 UtilService 클래스들에 있는 메소드들 성격이 비슷하지만, 해당 인터페이스의 구현체의 성격이 Util인 점을 감안하면 앞으로 추가 될 메소드의 성격이 갈라질 가능성이 많을 예정이라서 유연하지 않은 구조라고 판단 된다.
  3. 기능별로 세분화 한 클래스를 UtilService에서 필요 한 기능별로 다중 구현 하기 각 UtillService가 강제적으로 오버라이딩 해야 하는 메소드가 없기 때문에 필요 없는 메소드가 없을 것이다. 그리고 클래스의 기능에 맞게 세부적인 기능을 붙일 수 있어서 유연한 구조가 될 것이고, 테스트도 용이할 것으로 판단 된다.

 

결론

3번 방법을 채택 했다. 클래스 다이어그램은 다음과 같다. 각 handler 가 필요한 기능들을 의존해서 사용할 수 있고 각각 핸들러 테스트도 용이하게 된다.

 

 

그리고 EmailUtilService에만 있는 기능은 EmailUtilService 에 해당 기능을 이중 구현 해서 필요 없는 오버라이드를 막고, 테스트 용이한 유연한 코드가 될 것으로 판단했다.

 

'난중(개발)일기 > 깨달음' 카테고리의 다른 글

Testcontainer 도입기  (0) 2024.07.06
DTO vs VO  (0) 2024.06.10
API 설계를 하며 깨달은 점  (0) 2023.01.16
Rest API에 대한 깨달음  (0) 2023.01.07
Hexagonal architecture로 구조를 바꿔보며..  (0) 2022.10.04
728x90

DTO는 가변이고, VO는 불변이다.

DTO 는 인스턴스, VO는 리터럴 개념이다. 그래서 DTO는 setter가 있고, VO는 없다.

 

그래서 각 객체끼리 비교할 때 단순히 equals로 비교하면 DTO는 객체의 참조값(reference value) 을  비교하게 되고 VO는 객체에 포함 되어 있는 필드값 자체를 비교(equals를 오버라이딩) 해서 객체가 같은지 알 수 있다.

728x90
깨달음의 발단: https://github.com/f-lab-edu/SSKA/pull/47#discussion_r1070973660

 

 

나는 지금까지 어떤 기능을 만들 때는 꼭 해당 기능 맞춤형 API 하나가 만들어져야 한다고 생각했다. 그런데 그게 아니었다.

기능이 있다고 그 기능의 API 하나를 설계 해야 하는 것이 아니라 그 로직을 설계 해야 하는 것이었다.

 

우리가 Restful 하게 API를 만드는 것은 리소스를 생성, 수정, 삭제, 조회하는 것이지 특정 기능만을 위한 api는 아니다

 

이번 SKKA 프로젝트를 하면서 "좌석 시간 변경" 기능을 만들고 API를 붙이려고 할 때, 해당 프로젝트에는 API가 총 4개가 있는데 ("좌석 예약", "좌석 이동", "좌석 이용 시간 변경", "좌석 예약 취소/퇴실") 기능별로 꼭 하나의 API를 설계 해야 한다고 생각하고 있었던 것이었다.

 

 

이렇게 하다 보니 코드의 반복이 계속 되었다. 그러다 보니 코드가 복잡해지며 서비스 로직에서 하나의 메소드에서 하나의 역할에 충실하지 못한 메소드가 나와야했다. 이것을 해소하기 위해서는 코드의 재사용이 필요했다. 예를 들어 "좌석 이동" 기능을 개발할 때 여러 로직들이 다른 API 에서 사용되는 서비스, 도메인 로직이 겹쳐, 이를 해결하기 위해  해당 기능("좌석 이동")을 개발할 때 "좌석 예약 기능"을 사용하면 풀리는 문제였다.

 

즉, 

 

이런식으로 "좌석 예약" 기능을 이용하여 다른 기능을 하는 API를 추가하면 코드의 반복을 줄일 수 있고 코드의 재사용이 가능해지며 하나의 역할에 충실할 수 있는 구조가 되었다.

728x90

REST API의 근간

과거에 다음, 한미르 같은 시절에는 WAS가 없었고 Nginx, apache 같은 Web Server 만 있었다. Web Server 는 정적인 파일을 serving 해준다. 사용자가 이미지 던져달라하면 던져주고 html 던져달라하면 던져주는 방식이다.

 

옛날에는 “im.html”을 던져달라하면 나의 개인정보가 나오는 html파일을 던져주곤 했다. 그러다 보니 그 당시 서버는 html 파일 서버였던 것이었다. 다시 말해 그 당시에는 우리가 그 html에 접근하기 위해서는 url에 “user/im.html” 를 요청하면 파일을 달라고 요청하는 것이었다. 그러면 html 웹 서버가 디렉토리에서 뒤져서 파일을 찾아서 주는 것이었다.

 

만약 그 위치에 파일이 존재하지 않으면 not found를 던져주는 것이고 url에 잘못 쳐서 “user/em.html” 을 요청하면 양식이 맞지않다고 400에러를 준다. 여기 개념에서 부터 Rest API가 나온 것이다.

 

 

REST API 설계

html파일도 정적 Resource를 호출하는 것이기 때문에 Rest API 는 어떻게 보면 Resource를 호출하는 것이다. 그러니까 우리가 Rest API 를 만들 때도 가상의 디렉토리가 있다고 생각하고 Rest API를 설계한다고 생각하면 쉽게 된다. 밑에를 예를 들면 파일 서버 안에 shops라는 폴더가 있고, 그 안에는 1번 id를 가진 애가 있고 얘는 “싸다김밥”이고, 2번 id를 가진 애가 있고 얘는 “메가커피”이다. 그러면 디렉토리 구조라고 쳤을 때 다음과 같은 구조를 가지게 된다.

 

shops
├── 1
│   ├── 싸다 김밥
└───└── 메가 커피

이런 구조에서 1번 디렉토리에 있는 정보를 들고오고 싶으면

GET + /shops/1

를 해서 들고오면 되는 것이다.

 

그리고 shops라는 디렉토리 내에 새로운 무언가를 만들려고 하면

POST + /shops

를 요청하게끔 하면 되는 것이다.

 

업데이트와 삭제도 마찬가지이다.

PUT + /shop/1

DELETE /shops/1

이런식으로 하면 된다.

 

 

REST API 행위 구분

Rest API의 특징중에 “유니폼 인터페이스”라고 있다. 유니폼이란 같은 옷을 입고 있다는 뜻이다. 이것을 제어 하는 것은 Http method(GET, POST, PUT, DELETE) 로 행위를 제어해야한다. 즉 End point는 동일한데 Http method를 가지고 행위를 다르게 한다는 뜻이다.

 

 

응용을 해보면

teams
├── 1
│   └── 법무팀
├── 2
│   └── 홍보팀
├── 3
└───└── 영업팀

이 있고

teams
└── 1
    └── memebers
           └── 홍길동

트리구조로 이렇게 "법무팀에서 일하는 홍길동 사원을 찾고싶을 때"

GET + /teams/1/members/1

요청으로 찾으면 “홍길동”이 결과로 나오는 것이다.

 

결론은 “파일” 처럼 보면 된다. 그러면 Rest API 설계가 굉장히 쉬워진다.
728x90

헥사고날 아키텍처를 처음 접해보았을 때는 인턴때였습니다. 프로젝트 베이스가 깔려지고 사수님한테 설명을 듣고 "나도 저런 아키텍처를 내 코드에 사용해보고싶다.. 더 나아가서 아키텍처를 개발해보고 싶다" 라는 생각이 들었습니다. 마침F-lab을 하며 멘토님이 헥사고날 아키텍처를 사용하여 코드 작성하는 모습을 보여주었습니다. 인턴 때 예습이 되어있어서 그런지 정말정말 이해가 잘 되는 느낌이었습니다. 

 

 

 

헥사고날 아키텍처

  • 내부 영역 - 순수한 비즈니스 로직을 표현하며 캡슐화된 영역이고 기능적 요구사항에 따라 먼저 설계
  • 외부 영역 - 내부 영역에서 기술을 분리하여 구성한 영역이고 내부 영역 설계 이후 설계

이상 대략적인 설명이었고

자세한 설명은 https://netflixtechblog.com/ready-for-changes-with-hexagonal-architecture-b315ec967749 여기에 가서 보시길 바랍니다.. 괜히 잘못된 정보가 전해지면 안되니까요..

 

비즈니스 로직에 집중

내/외부를 분리하고 내부에는 비즈니스 로직들만 존재하고 외부에는 내부의 비즈니스 로직의 관심사가 아닌 것들(database, logging, etc...)이 존재하였습니다. 내/외부를 adaptor와 port를 이용하여 내부의 비즈니스 로직이 필요한 것들을 꽂아서 사용할 수 있는 구조였습니다. 다음 그림으로 설명이 될 것 같습니다.

포트(port)

 

어뎁터(adaptor)

USB를 옆의 그림처럼 USB 포트에 꼽아서 사용하는 것과 같다고 생각합니다. USB는 우리가 원하는 것들을 꼽아서 사용하지 않나요? 그런거와 같이 우리가 비즈니스로직에 MySql을 사용하고싶으면 MySql에 해당하는 어뎁터를 포트에 꼽고 H2를 사용하고 싶으면 그에 해당하는 어뎁터를 꼽아 사용할 수 있습니다. 

 

 

 

 

어뎁터는 포트에다가 꽂을 수 있는 것들을 골라서 꼽을 수 있습니다. 비즈니스 로직에 사용하고 싶은 어뎁터로 포트에 꼽으면 되는 구조였습니다.

 

 

 

 

 

 

 

 

 

어떻게?

인터페이스가 가능하게 해줍니다. 

 

원래 이런 구조였습니다. CommandHandler가 바로 MybatisRepositoryRepository를 의존하고 있었습니다. 그런데 사실상 CommandHandler는 MyBatis, JPA, JDBCtemplate 등 무슨 ORM을 사용하는지 비즈니스 로직의 관심사가 아닙니다. 그리고 바로 구현 객체를 의존하면 변경에 유연하지 않다고 생각합니다. 이런 상황을 풀어줄 친구는 인터페이스 입니다. 다음 사진으로 가보겠습니다.

 

이렇게 인터페이스인 IBookRepository를 의존하는 방법입니다. 여기서 인터페이스는 port 역할을 합니다. 그리고 BookMybatisRepository는 adaptor 역할을 합니다.

 확실하게 관심사가 분리될 수 있었습니다. 비즈니스 로직은 비즈니스로직이 중심이 되고 비즈니스 로직이 필요한 것들은 포트를 게이트로 삼아 끼워넣는 방식은 변경에 유연하다고 생각했습니다. 

 

 

 

 

프로젝트 구조 바뀌기 전 후

구조 바뀌기 전
구조 바뀐 후

 

 

훨씬 깔끔해졌습니다..!!!!

 

https://github.com/Taewoongjung/Doseoro_Java/pull/37

 

728x90

이와 같이 패키지 전체를 포인트 컷으로 잡아서 구현한 AOP를 Custom AOP로 바꾸어보았습니다.  

@Aspect
@Component
public class LogAspect {
    private static final Loggerlogger= LoggerFactory.getLogger(LogAspect.class);

    @Around("within(com.myproject.doseoro..*)")
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {

        String params = getRequestParams();

        long startAt = System.currentTimeMillis();

logger.info("----------> REQUEST : {}({}) = {}", pjp.getSignature().getDeclaringTypeName(),
                pjp.getSignature().getName(), params);

        Object result = pjp.proceed();

        long endAt = System.currentTimeMillis();

logger.info("----------> RESPONSE : {}({}) = {} ({}ms)", pjp.getSignature().getDeclaringTypeName(),
                pjp.getSignature().getName(), result, endAt-startAt);

        return result;
    }

    private String getRequestParams() {

        String params = "";

        RequestAttributes requestAttribute = RequestContextHolder.getRequestAttributes();

        if (requestAttribute != null) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                    .getRequestAttributes()).getRequest();

            Map<String, String[]> paramMap = request.getParameterMap();

            if (!paramMap.isEmpty()) {
                params = " [" + paramMapToString(paramMap) + "]";
            }
        }
        return params;
    }

    private String paramMapToString(Map<String, String[]> paramMap) {
        StringJoiner sj = new StringJoiner(",", "[", "]");
        return paramMap.entrySet().stream()
                .map(entry -> String.format("$s -> ($s)",
                        entry.getKey(), sj,join(entry.getValue())))
                .collect(Collectors.joining(", "));
    }
}

 

바꾼 이유는 공부 목적도 있었고 멘토님이 말하시길 "코드상 눈에 보이는 표식을 하지 않았기 때문에 모르는 사람이 볼때는 Aspect 로직이 왜 실행되는지 이해를 하지 못하는 경우도 많다"고 하셨다. 듣고 보니 정말 그럴 것 같았다.

 

어떻게 바뀌었냐면 

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Logging {}
@Aspect
@Component
public class LogAspect {
    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

    @Around("@annotation(Logging)") // Custom AOP
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {
    
    	// ... 이하 위 로직과 같음

 

이렇게 바꿈으로써 전에 없었던 어노테이션이 생겼고, 그 어노테이션으로 무엇을 하려고 하는지 정확히 알 수 있었습니다.

 

 

이 과정에서 @Transactional, @Cacheable, @Async 에너테이션도 AOP가 적용된 사례라는 것을 알게되었습니다.

 

 

참고

https://velog.io/@ann0905/AOP%EC%99%80-Transactional%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC

 

AOP와 @Transactional의 동작 원리

오늘은 @Transactional의 동작 원리를 AOP와 함께 좀 더 자세하게 조사해보려고 한다.여기서 다루는 내용은 다음과 같다.AOP란 무엇이며 왜 사용하는가Spring AOP는 왜 프록시를 사용하는가@Transactional은

velog.io

https://tecoble.techcourse.co.kr/post/2021-06-25-aop-transaction/

 

AOP 입문자를 위한 개념 이해하기

이 글은 AOP 개념이 생소한 입문자들을 위한 포스팅입니다. 1. OOP의 한계 image…

tecoble.techcourse.co.kr

https://private-space.tistory.com/98

 

Spring에서 AOP를 구현하는 방법과 Transactional

Spring에서 AOP를 구현하는 방법과 Transactional AOP에 관한 간략한 개념이 필요하다면 다른 글을 참조한다. 이 내용은 공식 문서를 참조하여 작성하였다. Spring Framework Document AOP 구현 방식 Spring에서 A..

private-space.tistory.com

 

728x90

버스를 타고 집에 오는 길에 생각이 나서 적어본다...

앞으로 얘기할 주제는 seq를 1,2,3,... 으로 하고 PK를 주고 id를 uuid를 넣어 UK를 주는 방식을 말한다.  

 

이 방법의 핵심은 이벤트 드리븐 형식에 있다고 생각한다. 아직 카프카 같은 것을 써보진 않았지만... (얼핏 봤을 때) 이벤트가 발생하고 그 이벤트가 큐에 들어갔을 때 이벤트의 고유번호를 테이블의 컬럼의 id를 주면 되겠다는 생각이 들었다.

 

그러면 나중에 모니터링 할때도 쉬워진다고 생각했다. 왜냐하면 그 이벤트가 발생하고 생겨난 row를 추적할 때 그 id를 가진 컬럼만 찾으면 되기 때문이라고 생각한다.

 

단지 생각만 나서 기록을 해둔다... 나중에 카프카나 RabbitMQ같은 메시징 서비스를 사용해볼 예정이라 그때 드는 생각을 또 적어보겠다.

728x90

Mybatis로 select를 해서 그 결과 값을 dto 객체에 넣으려고 했는데 기본생성자가 없다고하면서 오류가 났었던 기억이 있다.

@Builder
@Getter
@NoArgsConstructor
public class HomeDisplayedBookVO {

    private String id;
    private String title;
    private String about;

	public HomeDisplayedBookVO(
String id, String title, String about, String price, List<String> images) 
	{
        this.id = id;
        this.title = title;
        this.about = about;
			
			...

분명 @Builder 패턴으로 객체를 생성하면 되는데 왜 굳이 기본생성자가 필요한지 궁금했다.

Mybatis에서 resultType, resultMapping은 ObjectFactory라는 것을 사용하고 있다고 한다. 이 과정에서 객체의 초기화가 필요한데, 이 때 기본생성자가 먼저 호출되어 사용되어진다. 기본 생성자를 이용하여 오브젝트를 생성 한 후에는, 리플렉션을 사용하여 객체에 데이터를 바인딩 한다고 한다. 

 

그래서  리플랙션이랑 기본생성자랑 무슨 관계가 있는데?? 그리고 Mybatis랑 무슨 관계가 있는데? 

리플랙션에서 객체를 만들 때 기본 생성자를 먼저 찾는다고한다. 왜냐하면 먼저 객체를 생성해야 필드값들을 가져와서 필드값에 값을 넣어줄 수 있지않는가?? 객체를 만들 때 필요한 것기본 생성자이기 때문이다. 그러므로 Mybatis 로 select 하고 select 된 값들로 객체를 생성해야 하는데, 생성하는 과정에서 리플랙션(reflection)이 기본 생성자를 가르켜 객체를 생성한 후, 객체의 필드에 접근하여 값을 저장합니다.

 

 

참조

https://da-nyee.github.io/posts/woowacourse-why-the-default-constructor-is-needed/

 

[우아한테크코스] 기본 생성자가 필요한 이유 (Why the default constructor is needed) (feat. Jackson ObjectMapper

개요

da-nyee.github.io

https://okky.kr/articles/725359

 

OKKY - Spring MyBatis & DTO 기본생성자 관련 질문 드립니다.

안녕하세요.SQL Mapper인 MyBatis를 사용하다가 궁금한점이 생겨 질문 드립니다.MyBatis에서 Select를 통해 데이터를 조회 후 객체에 데이터 삽입할 경우 기본 생성자가 없을 경우 데이터값 삽입 불가 [@N

okky.kr

https://stir.tistory.com/52

 

기본 생성자(Default Constructor)가 필요한 이유

서론 어느날 갑자기 이런 생각이 떠올랐다. 클래스에 매개변수가 존재하는 생성자가 있으면 기본 생성자가 굳이 필요한가? 왜 매개변수가 존재하는 생성자를 만들어주면 항상 기본 생성자를 만

stir.tistory.com

 

728x90

MySql 테이블에 컬럼 타입이 JSON인 컬럼을 MyBatis로 읽어오려고 했는데 처음에 오류가 나서 12시간 가량 삽질 끝에 빛이 보였다. 이 과정을 공유하려고 한다.

 

문제

MySql 테이블은 다음과 같다.

CREATE TABLE `t_book`
(
		...
		
		`title` VARCHAR(100) NOT NULL,
		`about` VARCHAR(100) NOT NULL,
		`price` VARCHAR(100) NOT NULL,
		`img`   JSON             NULL,
		
		...
}

그리고 문제의 Mybatis 쿼리문은 다음과 같다

<select id="findHomeDisplayedBooks" resultMap="HomeDisplayedBookVO">
    SELECT
        title,
        about,
        price,
        img
    FROM t_book
    ORDER BY createdAt
    DESC  LIMIT 5
</select>

이것의 결과로 이렇게 나왔었다.

[HomeDisplayedBookVO{title='헤헤헿', about='나.', price='777', images=null}]

 

해결

<resultMap id="HomeDisplayedBookVO" type="com.myproject.doseoro.packages.book.vo.HomeDisplayedBookVO">
    <result property="title" column="title"></result>
    <result property="about" column="about"></result>
    <result property="price" column="price"></result>
    <result property="images" column="img" typeHandler="com.myproject.doseoro.global.util.JsonTypeHandler"></result>
</resultMap>
<select id="findHomeDisplayedBooks" resultMap="HomeDisplayedBookVO">
    SELECT
        title,
        about,
        price,
        img
    FROM t_book
    ORDER BY createdAt
    DESC  LIMIT 5
</select>

 

insert 처럼 typeHandler를 추가해주었더니 DB에서 JSON으로 데이터를 가져올 수 있었다.

[HomeDisplayedBookVO{title='헤헤헿', about='나.', price='777', images=[7df847a1-6846-4f60-9a52-7aed8677d02e.jpeg, 7e14f0f2-647f-4eff-b673-e518b2e96bd3.jpeg]}]

 

 

typeHandler 코드는 다음과 같다.

@MappedTypes({Object.class})
public class JsonTypeHandler extends BaseTypeHandler<List<String>> {

    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, List<String> s, JdbcType jdbcType) throws
            SQLException {
        preparedStatement.setObject(i, new Gson().toJson(s));

    }

    @Override
    public List<String> getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return convertToList(resultSet.getString(s));
    }

    @Override
    public List<String> getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return convertToList(resultSet.getString(i));
    }

    @Override
    public List<String> getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return convertToList(callableStatement.getString(i));
    }

    private List<String> convertToList(String myJsonDataListAsString) {
        try {
            return new ObjectMapper().readValue(myJsonDataListAsString, new TypeReference<List<String>>() {
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Collections.emptyList();
    }
}

+ Recent posts