728x90

 

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

Select

문제

MySql 테이블은 다음과 같다.

CREATE TABLE `t_book`
(
		...
		
		`title` VARCHAR(100) NOT NULL,
		`about` VARCHAR(100) NOT NULL,
		`price` VARCHAR(100) NOT NULL,
		`img`   JSON             NULL,
		
		...
}
<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으로 데이터를 가져올 수 있었다.

typeHandler 코드는 다음과 같다.

 

 

Insert

문제

MySql에 테이블은 select 할때와 같고, img 컬럼을 insert 하고싶었다. 오류가 계속 났다... 시도 끝에 해결책을 알아냈다.

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

 

해결

<resultMap id="BookVO" type="com.myproject.doseoro.packages.book.vo.BookVO">
        <result property="images" column="img" typeHandler="com.myproject.doseoro.global.util.JsonTypeHandler"></result>
</resultMap>
<select id="findBookByBookId" resultMap="BookVO" parameterType="String">
    SELECT
    	img,
    FROM t_book
    WHERE id = #{bookId}
</select>

이것 또한 typeHandler 를 넣었어야 했습니다. typeHandler의 코드는 위와(insert 때와) 같습니다.

 

마치며

해결하고 나니 너무 뿌듯했습니다. 고민하고 찾는 시간은 고됐지만 해결 후 뿌듯함은 더할나위 없이 기쁘네요. 

728x90

이런 오류가 나타났다.

 

이 오류는 dao.xml 에 중복되는 resultType이 있었기 때문이다. 처음에는 어차피 객체를 생성하는 틀이라고 생각해서 중복되는 VO나 DTO를 사용해도 될 것 같다고 생각했는데 아니었다. 그래서 다시 VO를 만들어주었다.

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

의심할바 없이 제네릭은 자바에서 헷갈리는 것중 하나입니다. 그리고 매일 자바로 프로그래밍을 하지 않는다면 쉽게 잊어버릴 수 있는 개념이기도 합니다. 예를 들어 List<?> 와 List<Object>는 비슷해 보입니다. 그러나 약간 다릅니다. List<?>는 기본적으로 어떤 타입(any type)의 List 입니다. List<String>, List<Integer> 등을 할당할 수 있습니다.

기본적으로 List<?>는 밑에와 같이 할당할 때 까지는 모릅니다.

List<?> list = new List<String>();

 

그러면 List<Object>로 돌아와봅시다. 이는 List<String>과 별다른 차이가 없습니다. String 대신에 Object를 받아들일 뿐입니다. 모든 클래스는 Object를 상속을 받고 있습니다. 그러면 본질적으로 Object는 어떤 객체든 담을 수 있다는 얘기입니다.

그런데 자세히 읽어보시면 List<?>는 어떤 List 타입을 할당받을 수 있고, List<Object>는 어떤 Object 타입을 저장할 수 있습니다. 이것이 진짜 List<?>와 List<Object>의 차이입니다.

예시를 한번 봐야겠지요?

 

제가 List<?>는 어떤 타입(any type)의 List를 할당할 수 있다고 했지만 List<Object>에는 그렇게 할 수 없습니다. 이와 같이 String, Integer, Double 등 List<Object>에 넣을 수 있는 것들을 다 넣을 수 있습니다. 그러나 List<?>에는 불가능합니다. 왜냐하면 <?>은 타입이 unknown이기 때문입니다.

import java.util.ArrayList;
import java.util.List;

public class Demo {

public static void main(String[] args) {

        printElements(new ArrayList<String>()); // OK
        printElements(new ArrayList<Integer>()); // OK

        printObjects(new ArrayList<String>());
        // NOT OK compile time error

        printObjects(new ArrayList<Integer>());
        // NOT OK compile time error
    }

public static void printElements(List<?> listOfUnknownType) {
        listOfUnknownType.add("abc"); // compile time error

for (Object o : listOfUnknownType) {
            System.out.println(o); // OK
        }
    }

public static void printObjects(List<Object> listOfObjects) {
        listOfObjects.add("abc"); // OK
        listOfObjects.add(101); // OK

for (Object o : listOfObjects) {
            System.out.println(o); // OK
        }
    }
}

위 코드를 보시면 ArrayList<String>와 ArrayList<Integer>는 printElements() 함수를 통과를 하지만 printObjects() 함수는 통과하지 못하는 것을 보실 수 있습니다. 왜냐하면 String과 Integer은 Object와 다른 타입이기 때문입니다. 부모-자식 상으로는 같지만, 제네릭은 불변 타입이기 때문에 상속관계를 무시하기 때문에 다르다고 인식이 됩니다. 첫번째 함수인 printElements()를 통과하는 것이 가능했던 이유는 어떤 타입을 받을 수 있는 List<?>이었기 때문입니다.

 

결국에는 printObjects(List<Object> listOfObject) 는 에러를 던지게 되는거죠 왜냐하면 Object 타입만 받을 수 있기 때문입니다.

만약에 단순 읽는 작업을 하면 List<?>와 List<Object>의 기능은 같습니다. 그런데 objects 를 추가할때는 List<?>는 에러가 납니다. 왜냐하면 unknown 이기 때문입니다.

 

List<?>의 경우 컴파일러는 List의 타입이 유효한지 모릅니다. 그래서 타입안정성을 보장할 수 없기 때문에 에러를 던집니다. 그런데 List<Object>의 경우에는 컴파일러가 List의 타입이 Object인것을 정확히 알기 때문에 String, Integer, Double 등을 할당할 수 있습니다.

 

그래서 List<?>와 List<Object>의 차이는 코더가 무엇을 하느냐에 달려있습니다. 만약 객체를 읽을때 (List에서 객체를 출력할때)  List<?>, List<Object> 둘다 사용해도 좋습니다. 그런데 요소를 추가할때는 List<Object>를 사용해야합니다. 만약 제네릭 함수를 작성중일 때 어떠한 타입의 list를 함수의 매개 변수로 받고싶을때는 List<?>를 사용합니다.

 

 

https://okky.kr/articles/354841

'언어 > 자바' 카테고리의 다른 글

List<?> 와 List 의 차이  (0) 2023.05.03
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();
    }
}
728x90

아래와 같이 인터페이스를 정했을 때

public interface ICommandHandler<T, R> {

    public R handle(T t);
}

 

리턴타입을 void로 짜야하는 로직이 있었다. 이럴 때 Void 로 넣어주면 됐었다.

제네릭에는 Object 클래스를 상속받는 클래스들만 올 수 있고 객체만 올 수 있다. 어떻게 보면 당연한 소리다. 넘길 때는 주소값이 있는 객체를 넘겨야 하는 것이기 때문이다.

 

int, boolean 등등 primitive type 으로 할 때도 wrapper class 인 Integer, Boolean .. 으로 제네릭에 넣어야 한다.

 

https://stackoverflow.com/questions/5568409/java-generics-void-void-types

728x90

문제

Error attempting to get column 'id' from result set

이런 오류가 발생했다. 너무 어처구니 없는 오류였다.

 

해결

BEFORE

<select id="loginCheck" resultType="boolean" parameterType="String">
    SELECT
        id AS id,
        email AS email,
        name AS name,
        nick_name AS nickName,
        phone AS phone,
        forgot_pw_question AS forgotPwQuestion,
        forgot_pw_answer AS forgotPwAnswer
    FROM t_identity
    WHERE email = #{email}
</select>

 

AFTER

<select id="loginCheck" resultType="HashMap" parameterType="String">
    SELECT
        id AS id,
        email AS email,
        name AS name,
        nick_name AS nickName,
        phone AS phone,
        forgot_pw_question AS forgotPwQuestion,
        forgot_pw_answer AS forgotPwAnswer
    FROM t_identity
    WHERE email = #{email}
</select>

 

느낀 점

생각 짧았다.. mybatis를 처음 하다보니까 인터페이스에 선언하는 것과 메소드에 선언하는 것을 잠깐 혼동하고 저 sql 문에 해당하는 값이 db에 있으면 true 없으면 false로 출력한다는 생각에만 사로잡혀서 resultType 을 boolean 으로 해놓았다. 그래서 이런 에러가 생겼던 것이었다. HashMap 으로 바꾸니 정상적으로 됐다.

728x90

문제

mybatis에서 실제 sql이 있는 xml 파일이 인터페이스로 된 파일(DAO) 에서 Param 으로 들어오는 값들을 @Param("")으로 지정을 해주지 않아서 인식을 못해 바인딩이 되지 않은 것 같다.

 

해결

BEFORE

Boolean loginCheck(String email);

 

AFTER

Boolean loginCheck(@Param("email")String email);

 

느낀 점

지금까지 전 처럼 사용했는데 이런 오류가 난거 보니까 잘못 써 온것같다.

728x90

문제

attempted to return null from a method with a primitive return type (boolean)

방금 개발을 하다가 이처럼 에러가 났다. 내가 반환하려고 하는 타입은 boolean 인데 null 로 반환되었기 때문에 이러한 에러가 발생했다.

 

해결

null 값도 받을 수 있는 Wrapper Class 를 사용하면 된다.

그래서 return type인 boolean 을 Boolean 으로 바꿔주었다. 정상적으로 동작한다.

 

느낀 점

자바의 기초를 다져놓고 스프링을 하니까 뭔가 많이 편하고 더 알아가는 것과 느끼는 것이 많아서 좋다 ㅎㅎ

다시 한번 기초가 중요하다는 것을 느꼈다!

728x90
바뀌기 전

 

원래 쿼리 매핑을 해주는 xml 파일인 "DoseoroDao.xml" 이 원래 dao 폴더에 있었고 application.yml 에는 밑에와 같이 있었다.

mybatis:
  config-location: classpath:mapper.xml

 

바뀐 후

 

 

Deoseoro.xml 파일을 resources 폴더로 옮기고 밑에와 같이 추가 하니까 정상적으로 실행 되었다..

mybatis:
  config-location: classpath:mapper.xml
  mapper-locations: classpath:DoseoroDao.xml

 

느낀 점

폴더 경로를 인식하는데 문제가 있는 것 같았다. 리소스 루트소스 루트의 경로 인식이 다른 것 같다. 이를 명심해야겠다..

 

 

참조

https://twofootdog.github.io/Mybatis-Invalid-bound-statement(not-found)-%EC%97%90%EB%9F%AC/ 

 

[Mybatis]invalid bound statement (not found) 에러 | 두발로걷는개

Mybatis 에러 원인 및 해결 방법

twofootdog.github.io

 

+ Recent posts