[프로젝트3] 4. 버그 수정

rin·2020년 6월 6일
0
post-thumbnail

목표
1. 오류를 수정한다.
2. 의도와 다르게 작동하는 부분을 수정한다.

실제 웹에서 요청 시 로직이 중단되는 버그

테스트 코드를 통과한 api 요청 작업들이 실행되지 않는 오류가 발생하여서 수정하였다.
로그인 요청부터 문제가 생겼는데 이전에도 다뤘던 (여기에서 확인) 예외가 발생되고 있었다.

Receiver class org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor does not define or inherit an implementation of the resolved method 'abstract java.lang.Class findDynamicProjection()' of interface org.springframework.data.repository.query.ParameterAccessor.

아티팩트에 jar 파일이 포함되어있지 않은건지 확인해보니 원하는 파일이 잘 들어가 있었다. 릴리즈 버전이 springframework 라이브러리의 버전으로 통일하기로 하였다.

build.gradle에서 spring-data-jpa를 제거해주었다.

compile group: 'org.springframework.data', name: 'spring-data-jpa', version: '2.2.6.RELEASE'

그리고 spring-data-mongodb의 버전을 2.2.6 버전으로 올려주자.

 compile group: 'org.springframework.data', name: 'spring-data-mongodb', version: '2.2.6.RELEASE'

그럼 applicationContext.xml 파일에서 문법오류가 날텐데, jpa 사용을 위해 작성했던 내용은 지워주면된다.
전체 내용은 아래와 같아진다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:mongo="http://www.springframework.org/schema/data/mongo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/context
                            http://www.springframework.org/schema/context/spring-context.xsd
                            http://www.springframework.org/schema/tx
                            http://www.springframework.org/schema/tx/spring-tx.xsd
                            http://www.springframework.org/schema/data/mongo
                            http://www.springframework.org/schema/data/mongo/spring-mongo.xsd">
    
    <tx:annotation-driven />

    <context:component-scan base-package="com.freeboard03.domain">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository" />
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Service" />
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Component" />
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    <mongo:mongo-client id="mongoClient" host="localhost" port="27017" />
    <mongo:db-factory id="mongoDbFactory" dbname="example" mongo-ref="mongoClient" />
    <bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
        <constructor-arg name="mongoDbFactory" ref="mongoDbFactory"/>
    </bean>
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <mongo:repositories base-package="com.freeboard03.domain" />


</beans>

테스트 코드를 돌려보자.
테스트 클래스에 @Transactional 어노테이션이 있었다면 아래와 같은 오류가 발생할 것이고,

java.lang.IllegalStateException: Failed to retrieve PlatformTransactionManager for @Transactional test:
	(... 중략)

	at org.springframework.util.Assert.state(Assert.java:94)
	at org.springframework.test.context.transaction.TransactionalTestExecutionListener.beforeTestMethod(TransactionalTestExecutionListener.java:199)

Service 클래스에만 @Transactional 어노테이션이 있었다면 아래와 같은 오류가 발생할 것이다.

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'transactionManager' available: No matching TransactionManager bean found for qualifier 'transactionManager' - neither qualifier match nor bean name match!

	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:72)
    
	(...중략)
    
    
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'transactionManager' available: No matching TransactionManager bean found for qualifier 'transactionManager' - neither qualifier match nor bean name match!
	at org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils.qualifiedBeanOfType(BeanFactoryAnnotationUtils.java:136)
	at org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils.qualifiedBeanOfType(BeanFactoryAnnotationUtils.java:95)
   
	(...후략)

하여간 문제는 TransactionManager가 없다는 것이고, @Transactional 어노테이션을 통해 Transactional하게 수행하기 위해선 TransactionManager라는 빈을 추가해야한다.

spring-data-mongodb를 위한 TransactionManager를 선언하는 방법을 찾아보던 도중, mongoDB에서는 트랜잭션을 지원하지 않는다는 것을 알게 되었다.🤔

❗️NOTE

MongoDB does not support transaction. Only changes applied to the same document are done atomically. A change applied to more than one document will not be applied atomically.

MongoDB는 트랜잭션을 지원하지 않는다. 동일한 document에서의 갱신만 원자적으로 수행된다. 둘 이상의 document에 적용되는 변경 사항은 원자적으로 적용되지 않는다.
ref. https://docs.jboss.org/hibernate/ogm/4.0/reference/en-US/html_single/#_transactions


아래는 MongoDB의 공식 도큐먼트에서 발췌한 내용이다.

In MongoDB, an operation on a single document is atomic. Because you can use embedded documents and arrays to capture relationships between data in a single document structure instead of normalizing across multiple documents and collections, this single-document atomicity obviates the need for multi-document transactions for many practical use cases. For situations that require atomicity of reads and writes to multiple documents (in a single or multiple collections), MongoDB supports multi-document transactions. With distributed transactions, transactions can be used across multiple operations, collections, databases, documents, and shards.

MongoDB에서 단일 document에 대한 작업은 원자적이다. 내장 document와 배열을 사용해 단일 document 구조에서 데이터 간의 관계의 스냅샷을 찍어둘 수 있기 때문에, 단일 document에 대한 원자성은 multi-document 트랜잭션을 수행할 필요가 없다. 다중 documents(단일 혹은 멀티 컬렉션에서)에 대한 읽기 및 쓰기의 원자성이 필요한 경우네는 MongoDB가 multi-document 트랜잭션을 지원한다. 분산 트랜잭션을 사용하면, 트랜잭션은 여러 작업, 컬렉션, 데이터베이스, document, 샤드에 걸쳐서 사용될 수 있다.

깔끔하게, 코드내의 모든 @Transactional 어노테이션을 지워주자.
테스트 코드가 통과하는 것을 확인할 수 있을 것이다.

File > Program structure > Artifact에 들어가 변경된 jar 파일을 추가/제거해 준 뒤 다시 톰캣을 실행하자.

ObjectId 타입 문제

문제 확인

웹에서 데이터 수정/삭제를 시도하니 400 에러가 발생하였다.
PUT http://localhost:8080/freeboard03/api/boards/[object%20Object] 400
DELETE http://localhost:8080/freeboard03/api/boards/[object%20Object] 400

수정 요청삭제 요청

요청 path에 해당 게시글의 고유 아이디가 들어가는데 이전에는 "Long" 타입이었던 것이 "ObjectId"로 변경되면서 아래와 같이 데이터가 넘어온 것을 확인 할 수 있었다.

테스트 코드를 통해 find를 통해 얻어온 document의 id가 어떤 형식을 띄는지 확인 할 수 있었고, toHexString()을 통해 간단하게 실제 ObjectId를 가져올 수 있었다.
그럼 findById에서 사용할 ObjectId는 어떻게 만들면 될까? 🤔
정답은 new ObjectId(${HexString}) 생성자를 사용하면 된다. 헥사값으로 필요한 값을 자동으로 만들어서 (timestamp, counter, randomValue1, randomValue2) 정확하게 일치하는 ObjectId를 생성해준다.

코드 수정

우선 클라이언트로 전달해 줄 객체인 BoardDto의 멤버 변수인 id의 타입을 ObjectId에서 String으로 변경해주자.

생성자에서도 board.getId()였던 부분을 board.getId().toHexString()으로 변경한다.

ApiController와 Service도 ObjectId 대신 String을 사용하도록 변경한다. 대신, 데이터베이스 쿼리에서 필드값으로 사용해야 하는 곳엔 반대로 new ObjectId(id)를 사용해서 ObjectId로 변환해준다.

다시 톰캣을 실행하고 웹에서 글 쓰기, 수정, 삭제 등을 테스트해보자!!

변경된 데이터만 추합해서 서버에 전달하기

이전 글의 마지막에서 언급한 내용인데, 글 수정시 클라이언트에서 변경된 값과 상관없이 모든 데이터를 전달하는 것을 "변경된 값"만 전달하는 것으로 수정할 것이다.

이전 코드는 위와 같이 새로운 글을 작성할 때와 수정할 때의 폼 데이터를 가져오는 로직(setBoardForm)이 동일하다. 이를 분리해 줄 것이다.

변수 data를 선언하고 if 문 내에서 서로 다른 함수를 호출한다. (getNewData() 는 이전의 setBoardForm() 과 동일한 코드인데 이름만 바꿨음.)

getUpdateData() 함수 내에서는 $.each를 이용하였다. nowBoardList에 저장되어있는 이전 데이터와 $modal에서 가져올 수 있는 폼 데이터를 비교하여 같지 않을 시에만 boardForm 객체에 추가해준다.

서버에서는 boardForm에 포함되어 있지 않은 필드에 대해선 null값을 할당할 것이고, 추후 서버에서 업데이트 쿼리를 날릴 때 이 필드는 사용되지 않을 것이다.

실제로 서버에서 전송된 쿼리는 다음과 같다.

{ "_id" : { "$oid" : "5edb6ab9d998ec4eb16793f7"}} 
and update: { "$currentDate" : { "updatedAt" : true}, 
	      "$set" : { "writer" : { "_id" : { "$oid" : "5edb3d307ef6941f576a6a6b"}, "accountId" : "yerin", "password" : "pass123", "role" : "NORMAL", "createdAt" : { "$date" : 1591426352222},
              "updatedAt" : { "$date" : 1591426352222}}, 
              "title" : "타이틀 수정하기"} 
             }

작성자 정보 (writer)는 세션에서 얻어온 값으로 항상 추가하도록 되어있고, updatedAt 또한 현재 시간으로 갱신되도록 되어있다.
contents와 title 필드만 사용자의 액션에 따라 변경 여부가 상이한데 이 경우에는 title만 변경했기 때문에 contents 필드든 사용되지 않았다.

변경된 코드는 다음과 같다.

var getNewData = () => {
    var boardForm = new Object();
    var $modal = $('#boardModal');
    boardForm.title = $modal.find('#title').val();
    boardForm.contents = $modal.find('#contents').val();
    return boardForm;
}

var getUpdatedData = () => {
    var boardForm = new Object();
    var $modal = $('#boardModal');

    $.each(nowBoardList[nowBoardIndex], function (attribute, value) {
        var modalValue = $modal.find('#'+attribute).val();
        if(typeof modalValue != 'undefined' && value != modalValue){
            boardForm[attribute] = modalValue;
        }
    })

    return boardForm;
}
        
        
$(document).on('click', '#saveBtn', function () {
    $('#requiredBtn').click();

    var method, url, data;
    if ($('#saveBtn').text() === WRITE) {
        method = 'POST';
        url = 'api/boards';
        data = getNewData();
    } else {
        method = 'PUT';
        url = 'api/boards/' + nowBoardList[nowBoardIndex].id;
        data = getUpdatedData();
    }

    if (requiredInputHaveNullSpec(data) == true) {
        return false;
    }

    $.ajax({
        method: method,
        url: url,
        contentType: 'application/json',
        data: JSON.stringify(data)
    }).done(function (response) {
        if (typeof response != 'undefined') {
            if (typeof response.code != 'undefined') {
                alert(response.message);
            } else {
                $('#boardModal').modal("hide"); //닫기
            }
        }
    })
})

전체 코드는 github에서 확인 할 수 있습니다.

profile
🌱 😈💻 🌱

1개의 댓글

comment-user-thumbnail
2021년 3월 4일

잘읽었습니다. 몽고db 버전 4부터는 multi document transaction이 지원됐다는데 spring이나 jpa와는 안되려나요?

답글 달기