SVN 삽질기

Java/VCS 2020. 3. 16. 17:39

SVN을 사용하고 있는 회사에서 소스 코드를 다운로드 받기 위해 STS 최신 버전, 그리고 Tortoise 최신 버전을 설치했다. SVN 주소를 입력한 후에 Checkout을 한 순간, 코드를 다운로드 받다가 계속 해시값이 맞지 않는다는 오류를 뿜어내면서 제대로 받아지지 않는 증상이 지속되었다.

인터넷으로 검색하니 대부분, cleanup 명령을 실행하라는 내용이 있었고, 그에 따라 cleanup > update.. 다시 오류 발생하면 cleanup -> update...

이러다보니, 소스 코드를 로컬로 다운로드 받는데만 시간이 엄청 소요되었고 스트레스는 엄청나게 쌓여갔다.

다른 사람들한테 물어보면 다들 잘 된다는데 나만 유독 안되니 더욱 미치고 환장할 지경이었다. 이 사람 저 사람 잘 되는지 물어보다가 어떤 한 분이 인텔리J에서 SVN과 상호작용할 때, 버전을 지정하는 것을 보았다. 설마?!

정확하게는 파악하지 못했지만, 인터넷을 좀 더 검색해보니, SVN 버전이 올라가면서 파일 포맷이 변경되었다고 한다. 앞에 언급하셨던 분은 1.7을 사용하고 있었는데. 내가 새로 설치했던 최신 버전들하고는 호환이 안되는것이 아닐까 싶었다.

호환되는 버전의 이클립스 플러그인과 TortoiseSVN을 찾아서 설치해보기로 했다. TortoiseSVN의 경우 다운로드 페이지에서 older 버전을 위한 링크가 제공되어서 쉽게 찾을 수 있었다. 해당 버전을 설치한 후, 위에서 실패했던 SVN 저장소에 접근해서 checkout을 해보니 오류 없이 모두 성공하였다.

호.. 정말 버전 문제 때문이었나보다. 

이번에는 이클립스 플러그인 설치에 도전해보았다.

먼저 이클립스 Marketplace에서 SVN을 검색하니 Subclipse가 나왔다. 근데 이걸로 설치했을 때 앞에 언급했던 오류들을 경험했다. 곰곰히 생각해보니 과거 Subversive 설치 했던 것이 떠올랐다. 이것은 설치 후 커넥터를 별도로 설치할 때 여러 클라이언트 버전을 깔았던거 같아서 도전해보기로 했다.

결과는? 실패다. Subversive를 설치해보려고 하니 해당 이클립스 버전에서는 지원하지 않는다고 떴다. 이클립스 버전을 낮은 것을 쓰기는 싫고... 어쩌지...

Subclipse 낮은 버전을 설치하면 되겠다 싶었으나, Marketplace를 통한 설치는 최신 버전만 가능해보였다. 그러다 문득, Marketplace 이전에 업데이트 URL을 통한 설치 방법이 생각나서 예전 버전 URL이 제공되는지 검색해보니 여러 개 등장! 1.7에 맞는 URL을 찾아서 subclipse를 설치하니 제대로 동작한다.

하.. SVN의 버전 차이라는 것은 전혀 예상하지도 못했다. 단지 다른 사람들이 파일을 올릴 때 깨진것 아닌가? 라는 것만 짐작했을 뿐이었다. 삽질하면서 엄청나게 열 받는 하루였지만, 해결을 했으니 그걸로 위안을 삼아야겠다.

YOUR COMMENT IS THE CRITICAL SUCCESS FACTOR FOR THE QUALITY OF BLOG POST


@Entity, @Query 삽질기

Java/JPA 2020. 3. 13. 14:13

최근 Spring Boot와 Spring Data JPA를 공부하면서 간단하게 게시판을 작성하는 예제를 살펴보았다. 이 분의 글이 설명도 잘 되어 있는 것 같아서 무작정 따라해보면서 공부하기로 했다.

게시글 입력 기능에 관한 테스트 코드 작성과 기능 확인, 웹 페이지 확인까지 잘 진행되었는데, 게시글 목록에서 문제가 발생했다(미리 밝혀두지만 참고한 글에 오류가 있는 것은 아니다. 내 무지에서 온 것으로 인한 것이다).

아래는 오류 내용이다.

java.lang.IllegalStateException: Failed to load ApplicationContext
  ...
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: 
  Error creating bean ...
  ...
Caused by: org.springframework.beans.factory.BeanCreationException: 
  Error creating bean with name ...
  ...
Caused by: java.lang.IllegalArgumentException: 
  Validation failed for query for method ...
  ...
Caused by: java.lang.IllegalArgumentException: 
  org.hibernate.hql.internal.ast.QuerySyntaxException: 
  ...
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: 
  tb_posts is not mapped [SELECT p FROM Posts p ORDER BY p.id DESC]
  ...
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException:
  Posts is not mapped
  ...

쭈욱 가다가 가장 마지막 부근에서 실제 오류 내용이 나와 있다. @Query 애노테이션에 지정된 구문 문법 오류이고 FROM에 지정되어 있는 tb_posts가 매핑 되어 있지 않다는 내용이다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity(name = "tb_posts")
public class Posts extends BaseTimeEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private long id;
  ...
public interface PostsRepository extends JpaRepository<Posts, Long> {
  @Query("SELECT p FROM Posts p ORDER BY p.id DESC")
  Stream<Posts> findAllDesc();
}

예제 코드를 그대로 따라했고 @Query에 지정한 구문에 오타가 있는 것인지 몇 번이고 확인했다. 그렇게 몇 번을 확인하고 실행을 했는데도 불구하고 계속 앞에 나온 예외와 마주쳤다.

검색해보니 @Query에 사용되는 쿼리는 JPQL이기 때문에 FROM에 명기되는 것은 테이블 명이 아닌 엔티티 이름이어야 하고 대소문자에 주의해야 한다는 내용들이 주였다. 그런데 위 코드를 보면 FROM 뒤에 엔티티 클래스 명을 적어 놓았고 대소문자도 제대로 구분해서 일치시켰는데 조차도 계속 예외가 발생해서 멘붕이 오기 시작했다.

그러다가 Stackoverflow에서 원인을 찾아낼 수 있었다.

Update : To be more precise , you should use the entity name configured in @Entity to refer to the "table" , which default to unqualified name of the mapped java class if you do not set it explicitly. (P.S. It is @javax.persistence.Entity but not @org.hibernate.annotations.Entity)

위에 내가 따라하면서 수정한 부분은 @Entity에 name을 이용하여 디폴트를 재정의 한 부분이다. @Entity가 클래스와 테이블명의 불일치 해결을 위해 사용되는 용도로 생각했었는데 잘못 알고 있었던 것이다.

아래와 같이 쿼리를 수정하니 제대로 실행되는 것을 확인할 수 있었다.

public interface PostsRepository extends JpaRepository<Posts, Long> {
  @Query("SELECT p FROM tb_posts p ORDER BY p.id DESC")
  Stream<Posts> findAllDesc();
}

아 이제 된거구나.. 라고 생각했는데 또 무엇인가 잘못된 것을 발견했다. 사실 DB에 생성해놨던 테이블 이름은 tb_post였다. 그런데 @Entity name와 쿼리 FROM 뒤에 적은 것은 tb_posts였다. 어???

원래 의도는 엔티티 클래스 명과 테이블 명이 다른 경우를 가정하고 작업을 진행해본 것이었는데, 의도와는 약간 다르게 동작한 것이다.  정상적으로 실행된 이유를 곰곰이 생각해보니, Spring Boot의 application.properties에서 아래와 같은 설정을 해놓았던 것이다.

spring.jpa.hibernate.ddl-auto=create-drop

이 옵션으로 인해 tb_posts라는 테이블이 생겼다가 사라졌고, SQL 실행 툴을 통해서는 tb_post가 존재하고 있기 때문에 착각을 했던 것이다. 또한 JPQL에서 FROM 뒤에 나오는 엔티티 명이 엔티티 클래스명과 다르다는 것도 마음에 들지 않는다. 

의도했던 몇 가지를 정리해보자.

1. 테이블 이름과 엔티티 명은 상이하므로 이를 반영해야 한다.

2. @Entity에 이름을 부여해서 1번을 맞출 수는 있지만, JPQL에서는 FROM 뒤에 명기되는 엔티티 명이 테이블 명처럼 나타나는 것이 마음에 들지 않는다. 엔티티 클래스 이름과 동일하기를 원한다.

그러다 문득 생각난 것이 @Table이라는 애노테이션이었다. 지금까지 삽질한 것이, 과거 @Table에서 봤던 내용과 @Entity에서 봤던 내용들이 뒤죽박죽 섞여서 머릿 속에 남아 있었던 것이다.

그렇기에 아래처럼 수정해보았다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@Table(name = "tb_post")
public class Posts extends BaseTimeEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private long id;
  ...
public interface PostsRepository extends JpaRepository<Posts, Long> {
  @Query("SELECT p FROM Posts p ORDER BY p.id DESC")
  Stream<Posts> findAllDesc();
}

1번을 충족시키기 위해 @Table 애노테이션을 사용하고 기존에 @Entity에 부여했던 name 속성을 제거하였다.

@Entity 속성을 제거하였으로 이에 따라 @Query의 FROM 뒤에 tb_posts를 엔티티 클래스명인 Posts와 일치하도록 수정하였다.

이렇게 설정을 바꾼 후 테스트 코드를 돌려보니 오류가 발생하지 않는 것을 확인하였다.

어설프게 알고 있다가 날린 시간이 얼마나 되었던지... ㅜㅜ

결론: @Entity의 name을 통해 설정된 엔티티 이름은 JPQL에서 동일하게 사용되어야 한다. 이 사실을 통해 알 수 있는 것은 이 속성은 테이블 이름과의 불일치 용도를 위한 것은 아닌 것으로 판단되며, 이런 목적을 위해서는 @Table을 사용해야 할 것 같다.

YOUR COMMENT IS THE CRITICAL SUCCESS FACTOR FOR THE QUALITY OF BLOG POST