Spring @Transactional 주의점

안녕하세요. 오늘은 Spring@Transactional 어노테이션을 사용할 때, 주의점에 대해 포스팅합니다.

 

Spring 에서 JPA 기술을 쓸 때 빼놓을 수 없는 기능중 하나는 @Transactional 입니다.

아래와 같이 많은 편리성을 우리에게 제공해주기 때문이지요.

  • transaction begin, commit을 자동 수행해준다.
  • 예외를 발생시키면, rollback 처리를 자동 수행해준다.

 

그렇기에, 많은 보일러코드를 줄 일 수 있기 때문에, 아래와 같이 편리하게 코드를 작성할 수 있습니다.

Note. 문제 상황을 억지로 만들기 위해 코드 효율성이 좋지 않을 수 있습니다.

 

문제는 예시에서 쓴 addBook 메서드를 Books 클래스 내부에서 사용할 때 발생 할 수 있습니다.

 

혹시, addBooks 메서드를 사용할 때, 어떤것이 문제가 될 수 있는지 예상이 가시나요?

문제는 addBooks 메서드 내부에서 호출하는 addBook 메서드의 @Transactional 어노테이션이 적용되지 않는 것입니다.

 

그렇기에, 해당 코드를 실행하더라도 Database에는 저장된 Book 정보에 Flag 컬럼에 정상적으로 업데이트 되지 않습니다.

bookRepositorysave 메서드는 자체적으로 @Transactional 어노테이션이 붙어있어, 정상적으로 코드가 수행이 되어 Database에 insert 를 수행합니다.

하지만, update 의 역할을 하는 book 객체의 변경감지가 동작하지 않아, Flag 컬럼에 업데이트가 되지 않습니다.

 

@Transactional 이 동작하지 않았는지는 Spring@Transactional 기능을 제공하는 방식에 대해 살펴볼 필요가 있습니다.

 

Spring @Transactonal 기능제공 방식

JPA 의 객체 변경감지는 transactoncommit 될 때, 작동합니다.

그렇기에 Spring@Transactonal 어노테이션을 선언한 메서드가 실행되기전, transaction begin 코드를 삽입하며

메서드가 실행된 후, transaction commit 코드를 삽입하여, 객체 변경감지를 수행하게 유도합니다.

 

Spring 의 코드 삽입 방법은 크게 2가지 방법이 있습니다.

  • 바이트 코드 생성 (CGLIB 사용)
  • 프록시 객체 사용

2가지 방법중 Spring 은 기본적으로 프록시 객체 사용 이 선택됩니다. 그렇기에 interface 가 반드시 필요합니다.

SpringBoot 는 기본적으로 바이트 코드 생성 이 선택됩니다. 그렇기에, 굳이 interface 가 필요없습니다.

만약 개발환경이 SpringBoot 라면 Books 인터페이스 없이 봐도 무방합니다.

 

원리는 이렇습니다. 프록시 객체로 우리가 만든 메서드를 한번 감싸서, 메서드 위 아래로 코드를 삽입 해줍니다.

 

그렇기에, 우리는 BooksImpl 자료형을 사용 할 때, 스프링이 제공하는 BooksProxy 객체를 사용하게 되며,

BooksProxy 객체가 제공하는 addBook 메서드를 사용해야만 transaction 처리가 수행되는것입니다.

 

@Transactonal 이 수행되지 않은 이유.

다시 아래의 예제를 살펴봅니다.

 

BooksProxyaddBooks 메서드를 수행하면, 아래와 같은 순서로 작동됩니다.

BooksProxy::addBooks -> BooksImpl::Book

 

즉, BooksImpl 내부의 코드(addBook) 가 수행 되기 때문에 해당 메서드는 프록시로 감싸진 메서드가 아니라는 점에서

@Transactonal 어노테이션 기능이 수행되지 않는다는 것입니다.

 

해결방법

사실 @Transactional 메서드를 내부적으로 사용하지 않는것이 근본적인 해결책입니다.

하지만 만약 굳이 사용해야 겠다면, 의존성 주입을 이용하여 Proxy 인스턴스를 자체적으로 가져와 사용하는 방법이 있습니다.

 

해당 코드는 Books 인터페이스를 이용하여 BooksProxy 인스턴스를 주입할 수 있도록 유도합니다.

생성자 주입을 사용하면, 순환 에러가 발생합니다. @Autowired 로만 해주세요. Autowired 싫다.

 

그 후, 자기 자신 즉 this 가 가지고 있는 순수한 addBook 메서드가 아니라 proxy 로 감싸진 addBook 메서드를 통해@Transactional 어노테이션 기능을 사용할 수 있게 됩니다.

 

정리하자면

  • @Transactonal 어노테이션 메서드는 클래스 내부적으로 사용하지 말고, 밖에서 사용하자. (프록시 객체 때문에)
  • 굳이 내부적으로 사용하려면, 자기 자신의 Proxy 객체를 사용하여 처리하자.

 

마무으리

혹시 틀린 내용이 있어서, 알려주신다면 감사하게 듣겠습니다. (_ _)

긴글 읽어주셔서 감사합니다!

 

포스팅이 도움 되셨다면, 커피 한잔 후원해주세요!
더 좋은 포스팅 작성에 큰 힘이 됩니다.

Buy me a coffeeBuy me a coffee
  1. GOD IS GOOD ALWAYS 2020.03.10 18:27

    오 제가 방금전에 transaction 에 대해 질문하고 왔는데

    제가 사용하고 있는게 @Transactional 어노테이션이었거든요

  2. JEONG_AMATEUR 2020.06.08 21:01 신고

    @Async도 그렇고 @Transactional도 그렇고 프록시로 동작하는 annotation에서는 항상 이 문제를 유의해야겠군요!

    정리 감사합니다~

  3. kingkk31 2021.01.29 14:23 신고

    내용 큰 도움되었습니다 감사합니다!

  4. hpotter1993 2021.05.25 21:52 신고

    혹시 BooksProxy가 addBooks메소드가 아닌, addBook 메소드를 실행하는 것 아닌가요?? BooksProxy에는 addProxy만 작성되어 있습니다!...

  5. tomato 2021.06.15 14:49

    좋은 글 정말 감사합니다.

    스프링을 공부하고 있는 학생인데 스프링 내부 동작 원리와 같은 이런 정보들은 스프링 공식 문서에서 얻을 수 있을까요?

    • Mommoo 2021.07.12 02:16 신고

      답변이 늦었습니다. ^^

      공식 문서를 전부 읽어본건 아니지만, Proxy 내용은 있었던거 같습니다.

      저는 공식문서는 필요할때만 읽는편이고... 정보가 필요할때는 먼저 구글 서칭과 서적으로 시작하는 편입니다.

  6. 리키타카 2021.06.27 16:14 신고

    안녕하세요 궁금한 점 있습니다!
    1] 클래스에 @Transactional 달아주면 안되나요?

    2] void addBooks(List<String> bookNames) 메소드에 트랜잭션을 달아주면 안되나요?

    저는 보통 클래스에 트랜잭션 어노테이션 달고 / 조회 api 인 경우에는 메소드에 (readOnly = true) 달고 사용해서요.
    (아니면 별도로 조회 전용 컨트롤러를 분리해서 만들던가)

    글 잘 봤습니다.

    • Mommoo 2021.07.12 02:15 신고

      안녕하세요^^
      답변이 늦었습니다.

      1) 클래스에 해당 어노테이션 명시해줄수 있습니다.! 그렇기 되면 클래스에서 사용하는 모든 메서드가 @Transaction 처리를 받게 됩니다.

      2) 붙여줘도 됩니다 ^^
      다만, 붙이게 되면 하나의 트랜잭션으로 취합이 되구요 개별 트랜잭션 N개로 수행이 되지 않습니다.

      예시는 addBooks 에 트랜잭션을 붙여버리면, 어쨋거나 통합 Transaction이 수행되기 때문에, addBook 트랜잭션이 수행되지 않는다는걸 알려드릴 수 없었습니다.




안녕하세요.

오늘은 Spring 프레임워크에 빼놓을 수 없는 라이브러리중 하나인 Jackson에 대해 간단하게 포스팅 합니다.

주의!! 해당 포스팅은 Jackson의 라이브러리 2.9.7 버전을 다룹니다.

또한, 프로젝트에 Jackson 라이브러리의 설치 방법에 대해서는설명하지 않습니다.

패키지 매니저를 쓰면 너무 간단하며, 구글에 검색해보면 엄청 많이 나와요우우

Jackson 라이브러리?

Spring 개발을 하다 보면, 컨트롤러 text/html 형식이 아닌 데이터 전달 목적으로 사용하고 싶을 때가 있습니다. 물론, 쌩 문자열인 plain/text 형식으로 보내도 상관은 없습니다만, 보통은 데이터 구조를 표현하는 방식인 XML 또는 JSON 형태로 많이 보냅니다.

데이터의 구조를 표현하는 이유는 데이터 표현도 있지만, 사실상 데이터를 사용하는 대상이 편하게 사용하기 위해서 입니다.

이 두개 중 JacksonJSON 데이터 구조를 처리해주는 라이브러리 입니다.

만약 JSON으로 데이터 구조를 표현 한다면, 아래와 같습니다.

{
   "name": "Mommoo",
   "age": 28,
   "isDeveloper": true,
   "equipment": ["NoteBook", "Mouse", "SmartPhone"],
   "etc": {
       "favoriteFood": 'Coffee'
  }
}

Jackson 라이브러리의 없이, JSON 데이터를 작성해보자.

Java에서 데이터를 저장할 때는, 아래와 같이 인스턴스를 사용합니다.

public class Person {
   private String name;
   private String job;
   
   public Person(String name, String job) {
       this.name = name;
       this.job = job;
  }
   
   public String getName(){
       return name;
  }
   
   public String getJob() {
       return job;
  }
}

public static void main(String[] args) {
   Person person = new Person("Mommoo", "Developer");
}

JSON 데이터를 어떻게 작성할까요? 그냥 작성해야 합니다... 아래와 같이요

미친짓이니 따라하지 마세요

String JSON = "\"{"+
   "\"name\": \"" + person.getName() + "\","+
   "\"job\": \"" + person.getJob() + "\""+
"}\"";

JavaSingle Quotes즉, 쌍따옴표("")를 정말 문자열로 사용하고 싶을때는 위와 같이 \문자를 붙여줘야 합니다. 끔찍한 코딩은 덤

항상 저렇게 코딩 할 순 없으니, JSON 변환용 클래스를 따로 만들고 그 클래스안에 저장된 멤버변수를 이용하여 JSON 데이터를 출력하는 클래스를 생각하게 됩니다.

대표적인 클래스가 Google 이 만든 GSON 또는 SimpleJSON 등이 있습니다.

SimpleJSON을 예로 든다면, 윗 코딩이 아래와 같이 바뀝니다.

JSONObject jsonObject = new JSONObject();
jsonObject.put("name", person.getName());
jsonObject.put("job", person.getJob());
String JSON = jsonObject.toString();

해당 방식은 지금도 많이 쓰입니다.

물론 Spring 컨트롤러에 위와 같이 코딩하여 리턴하더라도, 충분합니다.

그렇다면, Jackson은 무엇을 더 제공하길래 SpringJackson을 더 선호하는 것일까요?

Jackson과 기존 GSON or SimpeJSON과의 차이?

사실 차이는 없습니다.

JacksonObjectMapper API를 사용하여, 여타 GSON or SimpeJSON과 같이 객체에 데이터를 셋팅해줘야 하는건 마찬가지 입니다.

특별한 점은 Spring 프레임워크와 Jackson의 관계로부터 장점이 있습니다.

Spring 3.0 이후로부터, Jacskon과 관련된 API를 제공함으로써, Jackson라이브러리를 사용할때, 자동화 처리가 가능하게 되었습니다.

덕분에, JSON데이터를 직접 만들던가, GSON or SimpleJSON방식과 같이 직접 키와 벨류를 셋팅하는 방식에서 한단계 더 발전한 방식이 가능해졌습니다.

어떤 방식이길래 발전했을까요? 아래의 Jackson의 동작 원리와 함께 설명할까 합니다.

Jackson 은 어떻게 동작하는가?

Spring3.0 이후로 컨트롤러의 리턴 방식이 @RequestBody 형식이라면, SpringMessageConverter API 를 통해, 컨트롤러가 리턴하는 객체를 후킹 할 수 있습니다.

JacksonJSON데이터를 출력하기 위한 MappingJacksonHttpMessageConverter를 제공합니다. 만약 우리가 스프링 MessageConverter를 위의 MappingJacksonHttpMessageConverter으로 등록한다면, 컨트롤러가 리턴하는 객체를 다시 뜯어(자바 리플렉션 사용), JacksonObjectMapper APIJSON 객체를 만들고 난 후, 출력하여 JSON데이터를 완성합니다.

더욱 편리해진 점은, Spring 3.1 이후로 만약 클래스패스에 Jackson 라이브러리가 존재한다면, ( 쉽게 말해Jackson을 설치했느냐 안했느냐 ) 자동적으로 MessageConverter가 등록된다는 점입니다.

덕분에 우리는 아래와 같이 매우 편리하게 사용할 수 있습니다.

@RequestMapping("/json")
@ResponseBody()
public Object printJSON() {
   Person person = new Person("Mommoo", "Developer");
   return person;
}

이제는 그냥 데이터 인스턴스만 리턴 하더라도 JSON 데이터가 출력됩니다. 엄청나죠ㄷㄷ

위에서 설명한 방식보다 매우 진보한 방식인걸 알 수 있습니다.

다만, Jackson을 더 잘쓰기 위해서는 알아야 하는 기본 지식이 몇가지 존재합니다.

Jackson을 사용하기 위해 알아야 하는 기본지식

소개하는 기본 지식은 순전히 제 생각입니다... 의견이 다를 수 있습니다.

Jackson은 기본적으로 프로퍼티로 동작합니다.

Java프로퍼티를 제공하는 문법이 없습니다. ( 멤버변수랑은 다릅니다. )

Java프로퍼티는 보통 GetterSetter의 이름 명명 규칙으로 정해집니다.

문법적으로 정해지는것이 아니다 보니, 개발자가 일일이 신경써줘야 하는게 자바의 단점 중 하나입니다.

Person 같은 경우는 Getter만 존재 하므로, Getter를 기준으로 프로퍼티를 도출 할 수 있습니다. 즉 NameJobPerson 프로퍼티입니다.

Person의 멤버변수 이름도 똑같이 name, job이지만,

앞서 설명했드시 프로퍼티Getter, Setter기준이므로 멤버변수 이름을 변경하더라도 상관 없습니다.

갑자기 프로퍼티를 설명한 이유는 많은 라이브러리가 해당 프로퍼티 개념으로 작동하기 때문입니다.

Jackson라이브러리도 마찬가지 입니다. JSON데이터로 출력되기 위해서는 멤버변수의 유무가 아닌 프로퍼티 즉, Getter, Setter를 기준으로 작동합니다.

예로 아래와 같이 코딩하더라도 전혀 문제가 없습니다.

public class Person {
   public String getName() {
       return "Mommoo";
  }
   
   public String getJob() {
       return "Developer";
  }
}

@RequestMapping("/json")
@ResponseBody()
public Object printJSON() {
   return new Person();
}

결론적으로 Jackson을 사용한다면, Getter에 신경쓰셔야 합니다!!.

Jackson의 데이터 매핑을 Getter가 아닌 멤버변수로 하고 싶다면?

그렇다면, 이번에는 Jackson의 매핑을 프로퍼티가 아닌 멤버변수로 할 수 있는 방법은 무엇일까요? Jackson은 이와 관련하여 @JsonProperty 어노테이션 API를 제공합니다. 아래와 같이 멤버변수 위에 프로퍼티 이름과 함께 선언해준다면, JSON데이터로 출력 됩니다.

public class Person {
@JsonProperty("name")
   private String myName = "Mommoo";
}

위의 예시는 {"name": "Mommoo"}로 출력 됩니다.

그렇다면 JSON 매핑을 멤버변수로 하고 싶다면, 매번 @JsonProperty를 선언 해야 할까요? 귀찮습니다. 애초에 Jackson 매핑 구조를 바꾸면 어떨까 하는 생각이 듭니다.

Jackson의 데이터 매핑 법칙 변경하기

Jackson은 매핑 법칙을 바꿀 수 있는 @JsonAutoDetect API를 제공합니다.

위 예시와 같이 멤버변수로만 Jackson을 구성하고 싶은 경우 @JsonProperty를 일일이 붙이는 것보다 아래와 같이 설정하는 것이 더 편리합니다.

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class Person {
   private String myName = "Mommoo";
}

@JsonAutoDetect멤버변수 뿐만 아니라, Getter, Setter의 데이터 매핑 정책도 정할 수 있습니다. 만약 아래의 경우는 멤버변수 뿐만 아니라, 기본정책인 Getter역시 데이터 매핑이 진행됩니다.

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class Person {
   private String myName = "Mommoo";
   
   public String getJob() {
       return "Developer";
  }
}

Getter를 제외하고 싶다면, @JsonIgnore API를 쓰셔도 됩니다.

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class Person {
   private String myName = "Mommoo";
   
   @JsonIgnore
   public String getJob() {
       return "Developer";
  }
}

하지만, 역시 일일이 붙여야 하는 상황이 온다면 매핑 정책을 바꾸시는게 좋습니다.

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NON_PRIVATE)
public class Person {
   private String myName = "Mommoo";
   
   public String getJob() {
       return "Developer";
  }
}

Getter정책으로 private 만 데이터 바인딩에 제외 하였습니다.

이렇듯, 제외 범위를 설정할 수 있습니다. 자세한건 아래를 참고해 주세요.

https://fasterxml.github.io/jackson-annotations/javadoc/2.9/com/fasterxml/jackson/annotation/JsonAutoDetect.Visibility.html

Jackson의 데이터 상태에 따른 포함 관계 설정

만약 Jackson데이터 매핑시 NULL 값 과 같은 특정 데이터 상태인 경우 제외하고 싶다면 어떻게 해야 할까요?

Jackson은 이와 관련하여 @JsonIncludeAPI를 제공합니다.

NULL을 클래스 전반적으로 제외하고 싶다면, 클래스 위에 선언하면 됩니다.

또한 특정 프로퍼티NULL일때 해당 프로퍼티만을 제외하고 싶다면 역시 해당 프로퍼티위에 선언하면 됩니다.

@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
   private String myName = "Mommoo";
   
   public String getJob() {
       return "Developer";
  }
}

public class Person {
   private String myName = "Mommoo";
   
   @JsonInclude(JsonInclude.Include.NON_NULL)
   public String getJob() {
       return "Developer";
  }
}

JsonInclude.Include 속성은 NON_NULL뿐만 아니라 몇몇 개가 더 존재합니다.

자세한건 아래를 참고 해주세요.

https://fasterxml.github.io/jackson-annotations/javadoc/2.9/com/fasterxml/jackson/annotation/JsonInclude.Include.html

제가 준비한 Jackson 포스팅은 여기까지 입니다.

간단하게 하려 했는데 쓸때 없이 글이 길어진건 아닌가 싶네요.

긴 글 읽어주셔서 감사합니다.

포스팅이 도움 되셨다면, 커피 한잔 후원해주세요!
더 좋은 포스팅 작성에 큰 힘이 됩니다.

Buy me a coffeeBuy me a coffee
  1. 모두의개발 2020.07.16 09:11

    잘읽었습니다~

  2. 감사합니다 2020.12.08 12:16

    감사히 잘봤습니다

  3. Kyu Kim 2021.03.25 08:07 신고

    잘 읽었습니다 JsonProperty, JsonIgnore가 뭔지 궁금해서 찾아왔는데 그전에 Json이 뭔지 Jackson이 뭔지 잘 알게 된거 같아요. 오늘 TIL에 읽은글을 정리해보려했는데 작성하다보니까 그냥 거의 복붙이 되버렸네요.출처는 잘 표기했고 덕분에 잘 이해했습니다. 공부만하고있어서 커피는 못사드리지만 감사인사드리고 갑니다.

  4. 신입 2022.04.13 10:57

    잘 읽었습니다~

  5. MyeongDev 2022.06.16 21:35 신고

    글 너무 잘 읽었습니다.
    혼자 개발 공부중인 대학생입니다.
    @RestController 혹은 @ResponseBody 로 인해 Json 결과로 파싱되서 출력되는 것은 직접 실행시켜보고 본 글을 읽어 이해가 되었습니다.
    그런데 혹시 개발 할 때 데이터를 json 객체로 가공해서 넘겨주는게 맞는 걸까요?
    아니면 해당 라이브러리가 자동으로 변경 시켜주니 json 데이터로 가공하지 않고, 그대로 넘겨주는 것이 맞는 건가요?
    궁금증이 생겨 이렇게 댓글 남겨봅니다...

안녕하세요.

오늘은 MongoDB 데이터를 메서드로 주입하는 방법에 대해 포스팅 합니다.

노말한 방법은 MongoDB 데이터를 클래스 필드 이름이랑 매칭 시켜 주입합니다.

노말한 방법은 이전 포스팅을 참고해주세요!

SpringBoot와 MongoDB 연동하기

그런데, 만약 MongoDB데이터의 키 값과 클래스 필드 이름 매칭이 아닌, 클래스 필드 이름은 자유롭게 사용하고 싶을때는 어떻게 해야 할까요? 또는 데이터를 가공해서 넣고 싶을 때는 어떻게 해야 할까요?

즉 이번 포스팅은 MongoDB데이터를 먼저 받고 처리 하는것이 아닌 MongoDB 데이터를 받는 시점에서 처리하는 방법에 관한 것입니다.

@Field Annotation API 사용하기

위에서 언급한데로, 만약 MongoDB데이터의 키 값과 클래스 필드의 이름을 따로 사용하고 싶다면, @Field API를 사용하면 됩니다.

예를들어, MongoDB에 아래와 같은 Document를 가지는 Person Collection이 있다고 가정합니다.

{
   "name": "Mommoo",
   "job": "developer"
}

이 데이터를 주입 받는 Person 클래스를 아래와 같이 만들었다고 가정합니다.

@Document(collection="person")
public class Person {
   private String name;
   private String job;
}

위와 같은 상황이라면, 문제 없이 데이터를 주입 받습니다.

하지만 만약, Person의 멤버변수 namedeveloperName으로 바꾸고 싶다면 아래와 같이 @Filed API를 사용합니다.

@Document(collection="person")
public class Person {
   @Field("name")
   private String developerName;
   private String job;
}

@Field Annotation API를 이용하여 데이터 주입 메서드 만들기

문제상황 설정

Person Collection 정보를 위와 같이 따로 받지 않고, 아래의 NameCard 클래스로 받고 싶다면 어떻게 해야 할까요?

public class NameCard {
   private String developerName;
   priavte String job;
}

당연히 이런건, Person Collection에 NameCard 키를 추가 한 후 거기에 name과 job을 넣어 구조를 맞추는 경우가 많지만...

노말한 방법중 하나는 위의 Person 클래스로 데이터를 받고, NameCard를 뒤늦게 만드는 방법 입니다.

아래의 예시 처럼요.

@Document(collection="person")
@Getter //lombok 라이브러리 getter를 자동으로 만들어 줍니다.
public class Person {
   private String name;
   private String job;
}

public class Person2 {
   private NameCard nameCard;
   
   public Person2(NameCard nameCard){
       this.nameCard = nameCard;
  }
}

//Person을 주입 받은 어떤 메서드가 있다고 가정합니다.
public Person2 convertPersonToPerson2(Person p) {
  NameCard nameCard = new NameCard(p.getName(), p.getJob());
  return new Person2(nameCard);
}

해당 예시의 특징은 persistence class (Person)domain class(Person2)를 나누어서 사용하는 것을 알 수 있습니다. 즉, PersonDTO역할을 하며, Person2business logic역할을 합니다.

하지만, Spring이 추구하는 프로그래밍 패러다임중 하나인 POJO를 이루고자 한다면, 그렇게 좋은 구조는 아닌거 같습니다.

Plain Old Java Object : 그냥, 평범한 객체 오브젝트를 뜻합니다.

객체를 Data Holder로만 사용하면 도메인 영역을 처리하기 어려우니,

기존의 객체지향 방식으로 데이터 와 관련있는 기능들을 클래스에 넣음으로써, 도메인 영역을 더 효율적으로 처리하자는 의미입니다.

POJO방식으로 처리

위에서 언급한 DTObusiness logic을 한곳에서 처리 하기 위해서는 데이터 주입을 원하는 곳으로 받고 처리할 수 있어야 합니다.

아래와 같이 데이터 주입을 메서드로 받음으로써, 처리 할 수 있습니다.

public class Person {
  private NameCard nameCard = new NameCard();
 
  @AccessType(AccessType.Type.PROPERTY)
  @Field("name")
  public setName(String name) {
      nameCard.setDeveloperName(name);
  }
 
  @AccessType(AccessType.Type.PROPERTY)
  @Field("job")
  public setName(String job) {
      nameCard.setJob(job);
  }
}

마무리

생성자로 데이터를 주입받는 방법도 있지만, 해당 방법은 데이터에 대응하는 필드가 반드시 존재 해야 합니다. 그렇기에 위와 같이 필드가 아예 다를 때는 메서드로 주입 받아야 합니다.

주입받는 메서드의 접근 제어자는 무조건 public이어야 합니다. 그렇지 않으면 런타임 에러가 발생합니다.

Jackson 라이브러리를 사용 하신다면, 해당 setter 메서드 때문에 원하지 않게 JSON 구조로 잡힐때가 있습니다. 그럴 경우에는 아래와 같이 @JsonIgnore API를 사용하시면 됩니다.

public class Person {
   
   @JsonIgnore
   @AccessType(AccessType.Type.PROPERTY)
   @Field("name)
   public setName(String name) {
      ...
  }
}


포스팅이 도움 되셨다면, 커피 한잔 후원해주세요!
더 좋은 포스팅 작성에 큰 힘이 됩니다.

Buy me a coffeeBuy me a coffee



오늘은 SpringBoot 프레임워크에서 application.properties 파일을 읽는 방법에 대해 포스팅 합니다.

Java 로 파일을 읽을 때, 프로젝트 경로는 보통 아래의 두 API를 통해 읽습니다.

System.getProperty("user.dir") , [class].class.getResource("/").getPath()

직접 구현해보시면 아시겠지만, 많은 이유로 프로젝트 경로를 구하는 것은 생각보다 까다롭습니다.

예를들어, 멀티 모듈 프로젝트 또는 resource 폴더 위치에 따른 변동사항 등등 문제로 골치아픕니다.

하지만, SpringBoot@Value API 또는 Environment API 를 사용하면 편리합니다.

@Value API

클래스 변수위에 어노테이션으로 설정 할 수 있는것이 특징입니다.

간단하게 @Value(${프로퍼티 키})로 선언하면 됩니다.

예를들어, 웹 프로젝트의 컨텍스트 루트 경로를 코드상에서 알고 싶을때 아래와 같이 코드를 작성하면 됩니다.

public class ProjectContext {
   @Value(${"server.servlet.context-path"})
   private String contextPath;
   
   public String getContextPath() {
       return contextPath;
  }
}

문제는 Spring 생명주기 순서 문제로, null 값이 뜰 때가 있습니다.

만약 해당 문제점이 있다면, 아래에서 소개할 Environment API를 사용해야 합니다.

Environment API

Environment APIgetProperty(프로퍼티 키) 메서드를 사용합니다.

마치 JavaProperties API의 사용법과 비슷합니다.

마찬가지로 컨텍스트 루트 경로를 구한다고 가정해봅시다.

public class ProjectContext {
  private static final String CONTEXT_PATH_KEY = "server.servlet.context-path";
  private Environment environment;

  @Autowired
  public ProjectContext(Environment environment) {
      this.environment = environment;
      System.out.println(getContextPath()); // /Context-Path 출력
}

  public String getContextPath() {
      return environment.getProperty(CONTEXT_PATH_KEY);
}
}

Environment API@Value API 와 달리, Environment 객체가 안전하게 주입 되면,

안전하게 application.properties 을 읽을 수 있습니다.



포스팅이 도움 되셨다면, 커피 한잔 후원해주세요!
더 좋은 포스팅 작성에 큰 힘이 됩니다.

Buy me a coffeeBuy me a coffee



안녕하세요 오늘 포스팅은 SpringBoot와 MongoDB를 연동 방법입니다.


저는 최소한의 설정만을 원칙으로 하기 때문에, 연동에 필요한 부분만 알려드리고자 합니다.


SpringBoot 프레임워크가 이미 구현 해 놓은 API가 있어서 정말 간단합니다.


주의!!

해당 포스팅은 MongoDB Server 다운로드, MongoDB 개념, 사용법 등을 설명하지 않습니다.



SpringBoot 환경과 MongoDB 환경 정보


SpringBoot는 패키지 매니저로 Gradle(4.8.1)을 사용하구요, 

SpringBoot 프레임워크 버전은 2.1.0.RELEASE 입니다.

MongoDB는 윈도우10 x64 전용으로 4.0.4 버전입니다.



SpringBoot 설정


먼저 SpringBoot 프로젝트에 필요한 의존 라이브러리를 build.gradle 파일의 dependencies에 아래와 같이 추가합니다.


버전을 따로 기입 안함으로써, 2.1.0.RELEASE 버전이 제시하는 Default 버전의 라이브러리가 의존됩니다.


dependencies {

...
implementation('org.springframework.boot:spring-boot-starter-data-mongodb')
...
}


그다음, application.properties 파일에 MongoDB 정보를 아래와 같이 기입해야 합니다.


application.properties 파일은 보통 프로젝트/src/main/resource 경로에 있습니다.


spring.data.mongodb.uri=mongodb://[ip 정보]:[port 정보]
spring.data.mongodb.database=[데이터베이스 이름]


몽고 DB의 기본 정보는 localhost(127.0.0.1) ip와 27017 포트 구성을 가집니다.


MongoDB Server에 Database를 만드셨다면, 몽고 DB의 URI 예시는 다음과 같습니다.


mongodb://127.0.0.1:27017/MyDatabase


저는 데이터베이스 이름을 따로 설정했는데요, 아래와 같이 uri 정보에 한번에 기입할 수도 있습니다.


spring.data.mongodb.uri=mongodb://[ip 정보]:[port 정보]/[데이터베이스 이름]


해당 정보는 자바 파일로도 작성 가능합니다.

@Configuration
public class MongoDBConfiguration extends AbstractMongoConfiguration {
@Override
public MongoClient mongoClient() {
return new MongoClient("ip 정보", 포트정보);
}

@Override
protected String getDatabaseName() {
return "데이터베이스 이름";
}
}


MongoDB에 Document에 대응하는 DTO 클래스 만들기.


MongoDB Document가 아래와 같은 구조라 가정한다면,


Person Document


1
2
3
4
{
    "name""Mommoo",
    "job""Developer"
}
cs


DTO Class는 다음과 같이 작성됩니다.


Person.java


1
2
3
4
5
6
7
8
9
public class Person {
    private String name;
    private String job;
 
    @Override
    public String toString() {
        return "name is " + name + " job is " + job;
    }
}
cs


주의!!


보통은 Document 어노테이션을 통해, class와 매치되는 Collection을 맵핑 해주어야 합니다.

ex) @Document(collection="myCollection")


Document에 들어있는 모든 필드를 기입했다면, Document 어노테이션을 적지 않아도 됩니다만,

만약 필요한 필드만 기입하면 Document 어노테이션을 반드시 기술 해주어야 합니다.


DTO 클래스에 DB 데이터를 넣어 줄 Repository 객체 작성하기.


정말 간단하게, 아래와 같이 코딩하면 됩니다. 


특징으로는, MongoRepository 제네릭 부분에 앞서 작성한 DTO Class를 입력하면 됩니다. ( 예시로는 Person )


PersonMongoDBRepository.java


1
2
3
public interface PersonMongoDBRepository extends MongoRepository<Person, String> {
 
}
cs


몇몇 분들은 JPA 방식이라는 것을 눈치채셨을 겁니다. 


MongoDB의 Document 데이터를 호출하는 메서드는 기본적으로 MongoRepository에 이미 기술 되어 있습니다.


MongoDB의 쿼리랑 대응되는 메서드가 기본으로 존재합니다.

(findAll, insert 등등)


SQL문에 Where 조건절에 대응되는 쿼리를 작성하고 싶다면, MongoDB의 Query대로 method를 작성하면 됩니다.

예를들어, name 필드값 기준인 Document만 호출하고 싶다면 아래와 같이 작성됩니다.


findBy + 필드이름


1
2
3
public interface PersonMongoDBRepository extends MongoRepository<Person, String> {
    public Person findByName(String name);
}
cs


더 자세한 쿼리 예시를 보고 싶다면 아래의 사이트의 6.3 Query Methods 부분을 참고해주세요.

https://docs.spring.io/spring-data/mongodb/docs/1.2.0.RELEASE/reference/html/mongo.repositories.html



테스트 해보기.


아래와 같이 테스트 코드를 작성해서 호출한 데이터가 출력되는지 확인합니다.


SpringBootMongoDBTest.java


1
2
3
4
5
6
7
8
9
10
11
@RunWith(SpringRunner.class)
@SpringBootTest()
public class SpringBootMongoDBTest {
    @Autowired()
    private ProjectMongoDBRepository projectMongoDBRepository;
 
    @Test
    public void printProjectData() {
        System.out.println(projectMongoDBRepository.findAll());
    }
}
cs


저는 프로젝트/src/test/java/패키지 안에다가 테스트 class를 만들어 실행했습니다.



지금까지, MongoDB 연동 방법에 대해 알아봤습니다.


제가 소개한 방법은 MongoDB의 특징인 Query 조회와 비슷하게 접근 할 수 있는 방법입니다.

(JPA 방법이죠)


기존 MySQL와 같은 관계형 데이터베이스 방법대로 SQL문으로 조회하는 방법도 있습니다.

구글에 mongoDB template로 검색해보세요.


읽어주셔서 감사합니다.


포스팅이 도움 되셨다면, 커피 한잔 후원해주세요!
더 좋은 포스팅 작성에 큰 힘이 됩니다.

Buy me a coffeeBuy me a coffee
  1. ZUCCA 2019.03.29 02:04 신고

    혹시 출력 결과 화면도 함께 띄워주실수있나요?
    로그를 보면 연결은 되는것 같은데, mongoDB에 있는 json데이터는 안뜨네요

+ Recent posts