문제

https://leetcode.com/problems/count-triplets-that-can-form-two-arrays-of-equal-xor/

정렬된 n개의 음의 아닌 정수 배열 nums와 정수 maximumBit가 주어지고, 아래 쿼리를 n번 수행한다.

nums [0] XOR nums [1] XOR … XOR nums [nums.length-1] XOR k


를 최대화하는 k <2maximumBit의 음이 아닌 정수를 찾는데 이 때, k는 각 쿼리의 정답이다.
쿼리를 반복할 때 마다 배열 nums의 마지막 element를 제거하고, answer 배열을 반환하는데, 여기서 answer[i]는 i번째 쿼리의 정답 즉, k 이다.


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int[] getMaximumXor(int[] nums, int maximumBit) {
int[] pxor = getPrefixXOR(nums);

int[] answer = new int[nums.length];

int answerIndex = 0;
int maxValue = (1 << maximumBit) - 1;

for (int i = pxor.length-1; i > 0; --i) {

answer[answerIndex++] = pxor[i] ^ maxValue;
}

return answer;
}

private int[] getPrefixXOR(int[] nums) {
int[] psum = new int[nums.length + 1];

for (int i = 1; i <= nums.length; ++i) {
psum[i] = psum[i-1] ^ nums[i-1];
}
return psum;
}

흐름

  1. 누적합(prefix sum) 알고리즘을 통해 누적 XOR 배열을 구한다.
  2. 이 때, 2의 maximumBit승보다 작은 수 중에 가장 큰 수는 당연히 (2^maximumBit)-1 한 수 이므로, (2^maximumBit)-1 값을 변수에 할당해놓고,
  3. nums.length의 길이인 N 번 만큼 쿼리를 반복하면서 마지막 원소를 제거하면서 k 값을 구해 answer[i]에 저장한다.
  4. 끝.

pxor[i] ^ maxValue가 K인 이유

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void xorTEST() {
int x = 93;
int y = 12;
int z = x ^ y;

System.out.println("----- 초기값 -----");
System.out.println("x = " + x);
System.out.println("y = " + y);
System.out.println("z = " + z);

System.out.println("----- XOR -----");
System.out.printf("x ^ y = %d(z)\n", x^y);
System.out.printf("x ^ z = %d(y)\n", x^z);
System.out.printf("y ^ z = %d(x)\n", y^z);
}
1
2
3
4
5
6
7
8
----- 초기값 -----
x = 93
y = 12
z = 81
----- XOR -----
x ^ y = 81(z)
x ^ z = 12(y)
y ^ z = 93(x)

우리가 구해야 하는 값은 쿼리의 값을 가장 크게 할 값인 K이고, 우리는 이미 가장 큰 값을 (2^maximumBit)-1 로 구해놓았으므로,
누적합 알고리즘으로 구한 XOR값에 가장 큰 값을 XOR하면 k가 나오게 된다.


결과


댓글 공유

서론

패키지 구조를 수정 후, 기존에 정상 작동하던 custom repository에서 에러가 발생하여, 그 원인과 해결 방법에 대해 정리한다.


본론

코드의 구조는 아래처럼 되어 있다.

Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequiredArgsConstructor
@Service
public class ReviewFindService {

private final ReviewRepository reviewRepository;

public Review findById(long id) {
return this.reviewRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
}

public List<Review> findMyReviews() {
return this.reviewRepository.myReviews();
}
}

Repository

1
2
3
public interface ReviewRepository extends JpaRepository<Review, Long>, ReviewCustomRepository {

}

Custom Repository

1
2
3
4
public interface ReviewCustomRepository {

List<Review> myReviews();
}

Custom Repository Implementation

1
2
3
4
5
6
7
8
@Repository
public class ReviewCustomRepositoryImpl implements ReviewCustomRepository {

@Override
public List<Review> myReviews() {
return new ArrayList<>();
}
}

그림으로


Service에서 Repository를 선언하여 사용하고, Repository는 CustomRepository를 상속받고, CustomRepositroy를 CustomRepositoryImpl가 구현체로 작성된 상태이다. 이때, ReviewFindService의 findMyReviews()를 호출하면, 아래와 같은 exception이 발생하고, 코드가 정상적으로 작동하지 않았다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Caused by: java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List com.jgji.sokdak.domain.review.domain.ReviewCustomRepository.myReviews()! No property 'myReviews' found for type 'Review'
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:96)
at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:119)
at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateIfNotFoundQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:259)
at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$AbstractQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:93)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lookupQuery(QueryExecutorMethodInterceptor.java:103)
... 134 more
Caused by: org.springframework.data.mapping.PropertyReferenceException: No property 'myReviews' found for type 'Review'
at org.springframework.data.mapping.PropertyPath.<init>(PropertyPath.java:91)
at org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:438)
at org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:414)
at org.springframework.data.mapping.PropertyPath.lambda$from$0(PropertyPath.java:367)
at java.base/java.util.concurrent.ConcurrentMap.computeIfAbsent(ConcurrentMap.java:330)
at org.springframework.data.mapping.PropertyPath.from(PropertyPath.java:349)
at org.springframework.data.mapping.PropertyPath.from(PropertyPath.java:332)
at org.springframework.data.repository.query.parser.Part.<init>(Part.java:81)
at org.springframework.data.repository.query.parser.PartTree$OrPart.lambda$new$0(PartTree.java:250)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177)
at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)
at org.springframework.data.repository.query.parser.PartTree$OrPart.<init>(PartTree.java:251)
at org.springframework.data.repository.query.parser.PartTree$Predicate.lambda$new$0(PartTree.java:384)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177)
at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)
at org.springframework.data.repository.query.parser.PartTree$Predicate.<init>(PartTree.java:385)
at org.springframework.data.repository.query.parser.PartTree.<init>(PartTree.java:93)
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:89)
... 138 more

우선, 구현체인 ReviewCustomRepositoryImpl에 Break point 걸어보니, 아예 ReviewCustomRepositoryImpl의 myReviews()가 호출조차 되지 않는 것을 확인했다.
그 후 이것저것 코드도 수정해 보고 하였는데, ReviewCustomRepository를 따로 선언하여 사용하니 정상적으로 호출되었다.

Service에서 CustomRepository를 직접 참조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RequiredArgsConstructor
@Service
public class ReviewFindService {

private final ReviewRepository reviewRepository;
private final ReviewCustomRepository reviewCustomRepository;

public Review findById(long id) {
return this.reviewRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
}

public List<Review> findMyReviews() {
return this.reviewCustomRepository.myReviews();
}
}

기존에 계속 사용하던 구조이기에, CustomRepository를 선언하여 사용하는 방식으로는 문제 해결이 안 된다고 판단하여, 서론에서 말했던 것처럼 최근에 작업한 패키지 구조 수정 중 무언가 문제가 발생하였다고 판단되어 패키지 구조를 열어보니…



위 이미지처럼, Custom Repository interface와 구현체인 Custom Repository Impl 클래스가 서로 다른 패키지에 존재하고 있었고,



위처럼 같은 패키지 아래 존재 하도록 위치를 조정해주니 정상적으로 동작하는 것을 확인할 수 있었다.
interface는 기본적으로 접근제어자가 public으로 선언되는데, 패키지 위치가 다른 것이 어째서 문제가 되는지 찾아보니.. Spring 공식 문서에서 아래와 같은 가이드를 찾을 수 있었다.

The repository infrastructure tries to autodetect custom implementation fragments by scanning for classes below the package in which it found a repository. These classes need to follow the naming convention of appending a postfix defaulting to Impl.


결론

결론적으로, Custom Repository를 작성할 때는 구현체 클래스에 postfix로 Impl가 붙게 만들어야 하고, interface와 같은 위치, 혹은 하위 패키지에 존재하도록 하여야 한다는 것을 알았다.


참고 사이트

댓글 공유

서론

토이 프로젝트를 진행 중, 장소의 위치의 위도, 경도 좌표를 저장해야 했는데, float type의 ‘latitude’, ‘longitude’라는 2개의 컬럼을 만들어서 각각 따로 저장하려고 했는데, 우연찮은 기회에 공간 데이터 타입이라는 것이 따로 있다는 것을 발견하여 적용해본 경험을 글로 작성하여 보려고 한다.

본론

공간(空間, 영어: space)은 어떤 물질 또는 물체가 존재할 수 있거나 어떤 일이 일어날 수 있는 장소이다.


공간 데이터란, 위 같은 점, 선, 면의 공간을 데이터화 한 것을 의미하는데, 필자는 지도에서 위치를 표시하기 위해 2D 공간 데이터인 점(Point) 데이터를 저장할 필요가 있었다.
필자는 Spring boot + JPA(Hibernate) + H2 환경을 사용하고 있어서, 공간 데이터를 다루기 위해 hibernate-spatial 의존성을 추가해주었다.


1
2
3
4
5
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>${hibernate.version}</version>
</dependency>

그 후, hibernate-spatial 라이브러리에 포함된 The JTS Topology Suite를 사용해 좌표 데이터를 다루면 되는데, 위에서 말한 것처럼 필자는 2D 데이터를 다루기 위해 Point를 사용했다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import javax.persistence.Column;
import javax.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.locationtech.jts.geom.Point;

@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Address {

@Column(name = "road")
private String road;

@Column(name = "jibun")
private String jibun;

@Column(name = "zip", nullable = false)
private String zip;

@Column(name = "location", nullable = false)
private Point location;

@Builder
public Address(String road, String jibun, String zip, Point location) {
this.road = road;
this.jibun = jibun;
this.zip = zip;
this.location = location;
}
}
  • 만약 필자처럼 Point를 사용할 경우 org.springframework.data.geo.Point를 import하면 정상적으로 동작하지 않으므로, 꼭 import문을 확인하길 바란다.

위처럼 당차게 Entity class를 작성하고, 아래 같은 테스트 데이터를 넣고, 호기롭게 테스트를 돌려본 결과…

입력 데이터

1
2
3
4
5
6
7
8
9
10

2022-11-30 22:48:44.230 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [TIMESTAMP] - [null]
2022-11-30 22:48:44.231 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [TIMESTAMP] - [null]
2022-11-30 22:48:44.231 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [VARCHAR] - [지번 서울시]
2022-11-30 22:48:44.232 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [4] as [VARBINARY] - [POINT (19 2)]
2022-11-30 22:48:44.233 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [5] as [VARCHAR] - [도로명 서울시]
2022-11-30 22:48:44.234 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [6] as [VARCHAR] - [01006]
2022-11-30 22:48:44.235 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [7] as [BIGINT] - [null]
2022-11-30 22:48:44.235 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [8] as [VARCHAR] - [우리집]

  • 위 로그처럼 sql에 입력된 파라미터들이 보고 싶은 경우 아래처럼 설정을 추가해주면 된다.
1
2
3
4
5
6
7
logging:
level:
org:
hibernate:
type:
descriptor:
sql: trace

발생한 에러 로그

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Caused by: org.h2.jdbc.JdbcSQLDataException: Value too long for column "LOCATION BINARY VARYING(255)": "X'aced00057372001f6f72672e6c6f636174696f6e746563682e6a74732e67656f6d2e506f696e74... (1151)"; SQL statement:
insert into place (id, create_date, update_date, jibun, location, road, zip, category_id, name) values (default, ?, ?, ?, ?, ?, ?, ?, ?) [22001-214]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:506)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:477)
at org.h2.message.DbException.get(DbException.java:223)
at org.h2.message.DbException.getValueTooLongException(DbException.java:322)
at org.h2.value.Value.getValueTooLongException(Value.java:2573)
at org.h2.value.Value.convertToVarbinary(Value.java:1371)
at org.h2.value.Value.convertTo(Value.java:1125)
at org.h2.value.Value.convertForAssignTo(Value.java:1092)
at org.h2.table.Column.validateConvertUpdateSequence(Column.java:369)
at org.h2.table.Table.convertInsertRow(Table.java:926)
at org.h2.command.dml.Insert.insertRows(Insert.java:167)
at org.h2.command.dml.Insert.update(Insert.java:135)
at org.h2.command.CommandContainer.executeUpdateWithGeneratedKeys(CommandContainer.java:242)
at org.h2.command.CommandContainer.update(CommandContainer.java:163)
at org.h2.command.Command.executeUpdate(Command.java:252)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:209)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:169)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197)
... 136 more

테이블 생성 SQL

1
2
3
4
5
6
7
8
9
10
11
12
create table place (
id bigint generated by default as identity,
create_date timestamp,
update_date timestamp,
jibun varchar(255),
location varbinary(255) not null,
road varchar(255),
zip varchar(255) not null,
category_id bigint,
name varchar(255) not null,
primary key (id)
)

이처럼 예상하지 못한 결과가 발생하였는데… 로그를 보니, table create 시 좌표를 저장하는 컬럼의 타입이 기대했던 GEOMETRY이 아닌, varbinary 타입으로 생성되어서 발생한 에러인가 싶어서 컬럼 타입을 “GEOMETRY”로 지정하고 생성해봤지만, 결과는 마찬가지였다.

수정한 코드 및 실행된 SQL

1
2
3
4
5
6
7
8
9

...

// columnDefinition 속성으로 컬럼 타입 지정
@Column(name = "location", nullable = false, columnDefinition = "GEOMETRY")
private Point location;

...


1
2
3
4
5
6
7
8
9
10
11
12
create table place (
id bigint generated by default as identity,
create_date timestamp,
update_date timestamp,
jibun varchar(255),
location GEOMETRY not null, -- 정상적으로 컬럼의 데이터 타입이 GEOMETRY로 변경
road varchar(255),
zip varchar(255) not null,
category_id bigint,
name varchar(255) not null,
primary key (id)
)

다시 원인을 검색해본 결과… hibernate-spatial에서는 H2가 아닌 H2의 확장 데이터베이스인 GeoDB를 지원한다고 되어있는 것을 확인할 수 있었다.

The GeoDBDialect supports the GeoDB a spatial extension of the H2 in-memory database.

그 후 GeoDB를 적용하기 위해 properties 혹은 yaml에 아래처럼 dialect 설정에 org.hibernate.spatial.dialect.h2geodb.GeoDBDialect를 추가해 주면,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
datasource:
url: jdbc:h2:mem:test
username: sa
password:
driver-class-name: org.h2.Driver

jpa:
database-platform: org.hibernate.dialect.H2Dialect
defer-datasource-initialization: true
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
dialect: org.hibernate.spatial.dialect.h2geodb.GeoDBDialect # 추가 혹은 변경

dialect 설정 전 dialect

1
2022-11-30 23:37:47.975  INFO 15720 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect

dialect 설정 후 dialect

1
2022-11-30 23:35:42.124  INFO 18400 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.spatial.dialect.h2geodb.GeoDBDialect


이처럼 테스트가 성공하는 모습을 확인할 수 있었다.


참고 사이트

댓글 공유

서론

필자는 항상 단순하고 반복적인 일은 기계가 하고, 사람은 사색해야 한다고 생각한다. 당연하지만 이러한 생각은, 코딩할 때도 마찬가지다. class를 생성할 때마다 붙여야 하는 Annotation들을 (예를 들면 @Getter 같은) 붙이는 게 그렇게 귀찮을 수가 없었는데… 그러던 중 Intelli J의 Live Template 이라는 기능을 발견하여 필자 같은 사람들의 고통을 줄여 주고자 이 글을 작성한다.


본론


우선 위 이미지처럼 preference(settings)을 열어서 'Live Templates'를 검색해서 Live Templates 메뉴를 클릭한다.

열면 오른쪽에 여러 Template 그룹들이 존재하는데 필자처럼 Java가 없다면 Java 그룹을 생성해주거나, 기존 그룹에 탬플릿만 추가하여도 된다.


필자는 Entity 클래스를 생성하거나, request, response dto를 생성할 때마다

1
2
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)

annotation을 붙이는게 너무 귀찮아서 위 코드를 탬플릿으로 지정하고 단축키는 ‘gn’으로 설정해줬다.


그리고 해당 annotation들은 class에만 적용하면 되므로 Declaration으로만 지정해주었다.


모든 설정을 완료하고 이제 설정한 단축키를 지정한 위치에서 사용해보면 이처럼 간단하게 우리가 탬플릿화한 코드가 자동으로 생성되는 모습을 볼 수 있다.

이 글에선 소개하지 않았지만, template text를 입력하는 곳에서 $METHOD_NAME$ 처럼 $$ 으로 변수를 선언하여 사용하는 방법들도 있으니, 필요한 사람들은 꼭 활용하여 생산성을 높일 수 있기를 바란다.


결론

이렇게 우리는 또 작은 할 일과 짧은 시간을 기계에 넘기고 다른 생산적인 일을 할 시간을 얻을 수 있었다. 티끌 모아 태산이라는 말이 있듯이 이런 작은 일들이 줄여서 큰일을 할 수 있기를 바라며 글을 마친다.

참고 사이트

댓글 공유

발생 경위

기존 Spring 4.3.3을 사용하던 프로젝트를 5.3.22으로 업그레이드 이후 발생하였다.

에러로그 전체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'command' available as request attribute
at org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:153)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getBindStatus(AbstractDataBoundFormElementTag.java:178)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getPropertyPath(AbstractDataBoundFormElementTag.java:199)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getName(AbstractDataBoundFormElementTag.java:164)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.writeDefaultAttributes(AbstractDataBoundFormElementTag.java:123)
at org.springframework.web.servlet.tags.form.AbstractHtmlElementTag.writeDefaultAttributes(AbstractHtmlElementTag.java:460)
at org.springframework.web.servlet.tags.form.SelectTag.writeTagContent(SelectTag.java:405)
at org.springframework.web.servlet.tags.form.AbstractFormTag.doStartTagInternal(AbstractFormTag.java:87)
at org.springframework.web.servlet.tags.RequestContextAwareTag.doStartTag(RequestContextAwareTag.java:83)
at org.apache.jsp.WEB_002dINF.xxxx.xxxx.xxxx_jsp._jspService(new_jsp.java:263)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:476)
at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:386)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:330)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:728)
at org.apache.catalina.core.ApplicationDispatcher.doInclude(ApplicationDispatcher.java:591)
at org.apache.catalina.core.ApplicationDispatcher.include(ApplicationDispatcher.java:527)
at org.apache.jasper.runtime.JspRuntimeLibrary.include(JspRuntimeLibrary.java:868)
at org.apache.jasper.runtime.PageContextImpl.doInclude(PageContextImpl.java:679)
at org.apache.jasper.runtime.PageContextImpl.include(PageContextImpl.java:673)
at org.apache.tiles.request.jsp.JspRequest.doInclude(JspRequest.java:123)
at org.apache.tiles.request.AbstractViewRequest.dispatch(AbstractViewRequest.java:47)
at org.apache.tiles.request.render.DispatchRenderer.render(DispatchRenderer.java:45)
at org.apache.tiles.request.render.ChainedDelegateRenderer.render(ChainedDelegateRenderer.java:68)
at org.apache.tiles.impl.BasicTilesContainer.render(BasicTilesContainer.java:259)
at org.apache.tiles.template.InsertAttributeModel.renderAttribute(InsertAttributeModel.java:188)
at org.apache.tiles.template.InsertAttributeModel.execute(InsertAttributeModel.java:132)
at org.apache.tiles.jsp.taglib.InsertAttributeTag.doTag(InsertAttributeTag.java:299)
at org.apache.jsp.WEB_002dINF.xxxx.xxxx.xxxx_jsp._jspx_meth_t_005finsertAttribute_005f4(layout_jsp.java:10881)
at org.apache.jsp.WEB_002dINF.xxxx.xxxx.xxxx_jsp._jspService(layout_jsp.java:1242)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:476)
at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:386)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:330)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:728)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:470)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:395)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:316)
at org.apache.tiles.request.servlet.ServletRequest.forward(ServletRequest.java:265)
at org.apache.tiles.request.servlet.ServletRequest.doForward(ServletRequest.java:228)
at org.apache.tiles.request.AbstractClientRequest.dispatch(AbstractClientRequest.java:57)
at org.apache.tiles.request.render.DispatchRenderer.render(DispatchRenderer.java:45)
at org.apache.tiles.impl.BasicTilesContainer.render(BasicTilesContainer.java:259)
at org.apache.tiles.impl.BasicTilesContainer.render(BasicTilesContainer.java:397)
at org.apache.tiles.impl.BasicTilesContainer.render(BasicTilesContainer.java:238)
at org.apache.tiles.impl.BasicTilesContainer.render(BasicTilesContainer.java:221)
at org.apache.tiles.renderer.DefinitionRenderer.render(DefinitionRenderer.java:59)
at org.springframework.web.servlet.view.tiles3.TilesView.renderMergedOutputModel(TilesView.java:147)
at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:316)
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1404)
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1148)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1087)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.filters.ExpiresFilter.doFilter(ExpiresFilter.java:1228)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter.doFilterInternal(OpenEntityManagerInViewFilter.java:186)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:94)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:118)
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:84)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:113)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:103)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:113)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:146)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:154)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:45)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:199)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:199)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:57)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:354)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:543)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:678)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:609)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:810)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1623)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:829)

해결 과정

에러로그 일부

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	
...

java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'command' available as request attribute
at org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:153)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getBindStatus(AbstractDataBoundFormElementTag.java:178)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getPropertyPath(AbstractDataBoundFormElementTag.java:199)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getName(AbstractDataBoundFormElementTag.java:164)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.writeDefaultAttributes(AbstractDataBoundFormElementTag.java:123)
at org.springframework.web.servlet.tags.form.AbstractHtmlElementTag.writeDefaultAttributes(AbstractHtmlElementTag.java:460)
at org.springframework.web.servlet.tags.form.SelectTag.writeTagContent(SelectTag.java:405)
at org.springframework.web.servlet.tags.form.AbstractFormTag.doStartTagInternal(AbstractFormTag.java:87)
at org.springframework.web.servlet.tags.RequestContextAwareTag.doStartTag(RequestContextAwareTag.java:83)
at org.apache.jsp.WEB_002dINF.xxxx.xxxx.xxxx_jsp._jspService(new_jsp.java:263)

....


> Neither BindingResult nor plain target object for bean name 'command' available as request attribute

짧은 영어 실력과 구글 번역기에 도움으로 해석해보자면 BindingResult이나 ‘command’ 이라는 이름의 bean 을 request의 속성으로 사용할 수 없어서 발생하는 에러인 것 같은데,
에러 로그를 확인 한 결과… 위에서 보이는 것 처럼, JSP의 Form Tag에서 에러가 발생하는 것으로 추측되어 4.3.3 version에서 Form Tag를 구현하는 FormTag를 확인해보니 commandName 관련된 메서드들이 @Deprecated 로 선언되어서 곧 없어진다고 알려주고 있었는데, 이번에 업그레이드한 5.3.22 version의 FormTag에선 commandName의 getter, setter 메서드들이 없는 것을 확인할 수 있었다.


그리고 이미지를 보면 알 수 있듯이 기존의 setCommandName 메서드에서도 modelAttribute에 commandName을 저장해서 쓰는 것을 확인할 수 있는데, 5.3.22 에선 해당 메서드가 없어지면서 변수 modelAttribute에 ‘command’가 할당되어 있기에, ‘command’ 라는 bean을 속성으로 사용할 수 없다고 에러가 발생하는 것이었다.



이렇게 된 것이니 당연하게도 해결 방법은 modelAttribute에 우리가 지정한 속성값이 set 되도록 하면 되는데, 위에서 본 것처럼 기존의 setCommandName에서도 modelAttribute의 값을 사용하고 있었으므로, setModelAttribute로 modelAttribute 값이 저장되도록 하면 문제는 해결될 것이기에, JSP form tag에 commandName 속성을 모두 modelAttribute로 변경한 후 확인해 보면 정상적으로 동작 하는 것을 확인할 수 있을 것이다.



참고 사이트

댓글 공유

서론

현재 운영 중인 서비스는 같은 데이터베이스를 공유하는 5개의 프로젝트가 존재하는데, 모두 Spring or Spring boot + JPA를 사용하는 프로젝트들이다.

이 때문에, 한 프로젝트에서 도메인(Entity) 클래스에 변수를 추가한 경우에 다른 모든 프로젝트에서도 동일하게 추가 해주지 않으면 데이터베이스 조회 시 등 에러가 발생 할 수 밖에 없는데 이런 copy & paste 반복 작업을 개발자가 손수 하다보면 휴먼에러가 발생할 수 밖에 없음은 물론 같은 데이터베이스를 사용하는 Entity 임에도 불구하고 코드가 모두 다른 이상한 상황이 발생 할 수 밖에 없는 구조였다.

그렇기에 어떻게 할까 고민하던 중 멀티모듈 설계 이야기 with Spring, Gradle 이라는 글을 보고 필자의 서비스에도 단일 프로젝트 멀티모듈 구조로 가져갈 까 하다, 각각의 프로젝트마자 깃을 따로 관리하는게 서비스에는 맞다는 생각이 들어 멀티 프로젝트에 Nexus maven 저장소를 이용해서 도메인 클래스들을 분리 시키기로 했다.


본론

현재 시스템의 구조

이 처럼 여러 개의 프로젝트가 하나의 데이터베이스를 공유하는 구조로, 같은 데이터베이스 이므로 동일한 내용의 도메인 클래스가 각자 프로젝트에 각각 중복으로 존재하기 때문에 관리가 힘든 상황이다.

때문에 도메인 클래슬들만 모아놓은 새로운 프로젝트를 생성하고, 그 프로젝트를 jar파일로 만들어 라이브러리화 한 후, 내부 Maven 저장소인 Nexus에 업로드해서 다른 프로젝트들에서 dependency에 추가해서 시스템적으로 정합성을 유지하면서 사용 할 수 있게 하려고 한다.

간단하게 정리하면

  1. Nexus에 모듈화된 도메인 프로젝트 업로드
  2. 다른 프로젝트들에서 라이브러리화 된 도메인 프로젝트를 dependency 추가하여 사용

하는 것이 이번 글의 목표이다.

이 포스트에 Nexus를 설치하는 내용까지 담으려고 하니 내용이 너무 길어질 것 같아 nexus는 이미 설치되어 있다고 가정한다.


1. Nexus 설정

다른 분들의 글들을 보고 nexus를 무사히 설치하고 실행하면 아래와 같은 화면을 볼 수 있을 것이다.

그 후 Sign in 버튼을 클릭하면 초기 아이디와 비밀번호가 저장된 파일의 위치를 알려준다.


1
cat /설치경로/nexus3/admin.password


  • 설정 하다보면 아래처럼 익명 사용자도 접근하게 할 것인지 물어보는데 당연히도 수정 할 수 있으므로 적당히 선택해주면 된다.


2. Repository 생성

우선 모듈화한 도메인 프로젝트를 업로드 할 레파지토리부터 생성해야한다.



레파지토리 type은 group, hosted, proxy가 있지만, 우리는 위 처럼 maven2의 hosted로 생성한다.

  • 이때 hosted는 레파지토리 관리자가 직접 호스팅하는 레파지토리 type이고,
  • 다른 것들과의 차이점은 여기서 확인 해 볼 수 있다.



  • Version policy는

    • Release
    • Snapshot
    • Mixed

    3 가지가 존재하는데, 보통 Snapshot 레파지토리, Release 레파지토리를 따로 만들어 관리해야하지만, 필자는 Mixed로 지정하고 사용하였다.



  • Deployment policy는

    • Allow redeploy
      • 같은 버전으로 재배포 가능하고 덮어씀
    • Disable redeploy
      • 같은 버전으로 다시 배포 할 수 없음
    • Read-only
      • 아예 배포가 허용되지 않음
    • Deploy by Replication Only
      • 레플리카를 사용하는 경우 레플리카로만 배포되고 다른 배포는 차단됨
  • 자세한 내용은 여기서 확인 할 수 있다.



3. Repository access 유저 생성

Role 생성



필터란에 생성한 레파지토리명으로 검색해서 nx-repository-view-… 로 시작하는 권한들을 넣어주고, 해당 Role로 레파지토리에 업로드도 할 수 있게 하기 위해 nx-component-upload 권한도 넣어준다.

유저 생성



  • 각 항목들은 각자에 맞게 넣어주고 Role엔 조금 전에 생성한 Role을 넣어주고 생성을 완료하면 우리가 처음에 로그인한 어드민 계정과는 달리 우리가 생성한 repository만 접근 할 수 있는 것을 확인 할 수 있다.


4. 라이브러리 업로드

settings.xml 설정

nexus 레파지토리에 접근하기 위해서는 우선, maven 설정 파일인 settings.xml을 수정해야야 한다. 기본적으로는 ~/.m2/settings.xml 으로 설정되어 있으므로 이 곳에 생성해서 사용하거나, 이미 settings.xml을 사용하고 있다면 그 파일을 수정해주면 되는데.. 이도 저도 모르겠으면 아래 이미지 처럼 인텔리제이에서 확인하여 그 위치의 settings.xml을 수정해준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
http://maven.apache.org/xsd/settings-1.0.0.xsd">

<servers>
<server>
<id>junggu-demo-repository</id>
<username>junggu-demo-user</username>
<password>qweqwe</password>
</server>
</servers>
</settings>
  • id : 레파지토리 서버의 ID (자유롭게 지정)
  • username : 레파지토리 접근을 위해 생성했던 nexus access 계정 Id
  • password: 레파지토리 접근을 위해 생성했던 nexus access 계정 password

코드 작성

모듈화할 코드를 작성하면 되는데, 우리는 도메인 영역을 모듈화 할 것이므로 Entity 클래스를 생성해서 테스트 해본다.


POM.xml 설정

코드 작성을 완료했으면 이제 해당 프로젝트를 nexus repository에 업로드 하기 위해 pom.xml에 아래 처럼 설정을 추가해줘야한다.

1
2
3
4
5
6
7
<distributionManagement>
<repository>
<id>junggu-demo-repository</id>
<name>junggu nexus repository~ name is free</name>
<url>http://localhost:8081/repository/junggu-nexus-demo/</url>
</repository>
</distributionManagement>
  • id : settings.xml에서 설정했던 id와 동일해야함.
  • name : 사람이 식별 할 수 있게 설정.
  • url : 생성한 repository의 url

  • <distributionManagement> 와 <repository>대한 자세한 설명은 이 쪽으로

배포하기

1
mvn clean compile deploy
  • 위 명령어로 deploy 하거나 혹은 Intellij에서 deploy를 실행시켜도 된다.


  • 무사히 deploy가 성공했다면 nexus에서 jar 파일이 정상적으로 업로드된 것을 확인 할 수 있고, 이 후엔 이 라이브러리화된 jar파일을 다른 프로젝트들에서 dependency에 추가해서 사용하면 된다.


업로드된 라이브러리 사용하기

업로드된 라이브러리를 사용하기 위해서 사용하고자 하는 프로젝트의 pom.xml에 아래와 같은 설정을 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...


<!-- 모듈화되서 업로드된 도메인 프로젝트 -->
<dependency>
<groupId>com.junggu.ji</groupId>
<artifactId>demo</artifactId>
<version>0.0.1</version>
</dependency>

...


<!-- 라이브러리가 업로드된 Nexus Repository 정보 -->
<repositories>
<repository>
<id>junggu-demo-repository</id>
<name>junggu nexus repository</name>
<url>http://localhost:8081/repository/junggu-nexus-demo/</url>
</repository>
</repositories>
  • 우선 라이브러리가 업로드된 nexus repository의 정보를 입력하고
  • 우리가 업로드한 라이브러리 정보도 dependency에 추가해준다.
  • 이 때, dependency의 속성들이 헷갈린다면, nexus에서 업로드된 jar를 선택 할 경우 아래 이미지처럼 친절하게 알려주므로 복사해서 사용하면 된다.


Entity 호출하기


  • 위 이미지 처럼 dependency 추가 후 maven을 새로고침하면 보이는 것 처럼 추가도 잘 되고 코드에서도 사용 할 수 있는 것 처럼 보이는데 막상 코드에서 사용하려고 하면 아래 이미지에서 보는 것 처럼 정상적으로 호출되지 않는 것을 알 수 있다.


  • 결론부터 이야기하면 우리가 jar로 만든 프로젝트는 Spring boot 프로젝트를 그대로 jar로 패키징 한 것으로, 일반적인 jar 파일이 아닌 Spring boot가 실행 가능한 jar 형태로 패키징 되었기 때문에, jar에 포함된 클래스들을 호출 할 수가 없는 것이다.
    • 여기서 말하는 실행 가능한 jar는 META-INF에 저장된 MANIFEST.MF에 저장된 정보를 읽어서 실행시키기 때문에 우리가 일반적으로 호출하는 형태로는 사용 할 수 없다.



이 실행 가능한 jar를 일반적인 jar로 만들어서 repository에 배포하기 위해서 모듈화하는 프로젝트의 pom.xml에서 아래 설정을 제거 해주고 다른 dependency들에게 각각 version을 지정한 후 maven compiler plugin도 알맞게 추가해주면 되는데

이 때 우리가 삭제한 spring-boot-parent에 포함된 의존성들은 여기서 확인 할 수 있으므로, 필요한 것이나 각각 dependency들을 무슨 버전으로 지정해야 할 지 모르겠다면 여기서 확인 후 추가해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<plugins>

...


<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
<compilerArgument>-Xlint:all</compilerArgument>
</configuration>
</plugin>

...

<plugins>
  • 이제 verison을 올린 후 새로고침 해보면 이전과 다르게 BOOT-INF가 없어지고 바로 com. 으로 시작하는 자바 패키지가 노출되는 것을 확인 할 수 있다.


이제 테스트 코드를 작성해서 돌려보면 테스트가 성공 할 줄 알았으나.. 애석하게도 아래와 같이 Not a managed type 라는 에러를 내뿜으며 우리의 기대를 무참히 박살내는데…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Caused by: java.lang.IllegalArgumentException: Not a managed type: class com.junggu.ji.demo.domain.Junggu
at org.hibernate.metamodel.internal.MetamodelImpl.managedType(MetamodelImpl.java:582)
at org.hibernate.metamodel.internal.MetamodelImpl.managedType(MetamodelImpl.java:85)
at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.<init>(JpaMetamodelEntityInformation.java:75)
at org.springframework.data.jpa.repository.support.JpaEntityInformationSupport.getEntityInformation(JpaEntityInformationSupport.java:66)
at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getEntityInformation(JpaRepositoryFactory.java:233)
at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:182)
at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:165)
at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:76)
at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:325)
at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.lambda$afterPropertiesSet$5(RepositoryFactoryBeanSupport.java:323)
at org.springframework.data.util.Lazy.getNullable(Lazy.java:231)
at org.springframework.data.util.Lazy.get(Lazy.java:115)
at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:329)
at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.afterPropertiesSet(JpaRepositoryFactoryBean.java:144)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800)
... 85 more

이는 우리가 추가한 라이브러리의 Entithy가 component scan에 인식되지 않아서 Bean context에 등록되지 않았기 때문에 발생하는 에러이다.

왜냐면, spring boot는 기본적으로 @SpringBootApplication안에 있는 @EnableAutoConfiguration으로 스프링이 정의한 외부 의존성들과 @ComponentScan으로 해당 어노테이션 하위 패키지에 있는 클래스들을 탐색해서 Bean으로 등록하는데,

우리가 추가한 라이브러리는 둘 중 어느 위치에도 존재하지 않으므로 당연히 Bean에 등록 될 수없는 것이다.

라이브러리안에 있는 Entity Bean context 등록하기

여러 방법이 있겠지만, 필자는 SessionFactory Bean을 따로 만들어서 우리가 만든 Entity가 있는 라이브러리의 패키지까지 스캔 하도록 추가하는 방식을 사용하였다.

1
2
3
4
5
6
7
8
@Bean
public LocalSessionFactoryBean entityManagerFactory(DataSource dataSource) {
LocalSessionFactoryBean localSessionFactory = new LocalSessionFactoryBean();
localSessionFactory.setDataSource(dataSource);
localSessionFactory
.setPackagesToScan("com.junggu.ji.usenexus", "com.junggu.ji.demo");
return localSessionFactory;
}

적당히 작성한 테스트 코드와 성공한 모습
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource("classpath:application-test.properties")
@Import(DatabaseConfig.class)
@DataJpaTest
public class NexusTest {

@Autowired
private JungguRepository jungguRepository;

private final String name = "지중구";
private final int age = 30;
private final Gender gender = Gender.MALE;

private Junggu junggu = null;

@BeforeEach
void setUp() {

junggu = Junggu.builder()
.name(name)
.age(age)
.gender(gender)
.build();
}

@Test
void save() {
Junggu when = jungguRepository.save(junggu);

assertThat(when.getName()).isEqualTo(junggu.getName());
assertThat(when.getAge()).isEqualTo(junggu.getAge());
assertThat(when.getGender()).isEqualTo(junggu.getGender());
}
}


결론

이렇게 우리는 여러 프로젝트에서 공통으로 사용되는 부분을 따로 빼서 모듈화 하는 방법을 알아보았다.

필자는 단순히 Entity 클래스의 관리를 편하게 하기 위해 이번 모듈화를 진행했지만, 레이어 별로 프레젠테이션 레이어, 서비스 레이어, 도메인 레이어등으로 구분하여 모듈을 구분 할 수도 있을 것 이다.

다만, 이렇게 멀티 프로젝트에 nexus를 이용해서 모듈화를 구현하는 경우, 모듈화한 프로젝트를 수정 했을 때 모듈화된 라이브러리를 사용하는 다른 프로젝트 모든 프로젝트에서도 손수 version을 올려줘야한다는 단점이 있기 때문에(물론 Deployment policy를 Allow redeploy로 설정했다면 상관없다.), 각자의 서비스에서 단일 프로젝트 멀티 모듈 구조가 맞을 지 아니면 필자처럼 멀티 프로젝트인 상태로 모듈화를 구성 할지는 잘 판단해서 구조화를 해야 할 것 이다.


P.S

여담으로.. 필자는 이번 모듈화를 진행하면서 DTO에 의존적인 Entity들에 대해 굉장히 많이 리팩토링을 진행 할 수 있었고, 레이어 별 분리에 대해 다시 한번 생각해보는 계기가 되어서 고생은 좀 많이 했지만, 개인적으로 굉장히 뜻 깊은 시간을 보낼 수 있었다.


참고 사이트

댓글 공유

서론

어쩌다 보니 연속해서 @RequestBody에 관한 글을 작성하게 되었는데… 이전에 말했던 것 처럼 @RequestBody는 Jackson 라이브러리를 사용하기 때문에 기본생성자를 이용해 object(DTO)를 바인딩한다. 그 때문에 다른 생성자에서 validtion등을 처리했다고 해도 당연히 처리되지 않는데 이 때 사용할 수 있는 @JsonCreator에 대해 알아본다.


본론

현재 상황



위 이미지들 처럼 Controller와 dto를 만들었다고 했을 때, /test 를 호출해보면 어떻게 될까?


@RequestBody를 사용 할 경우 Jackson 라이브러리를 사용해서 기본 생성자를 이용해 매핑시키기 때문에 “여기타고 생성이 될까요?” 라는 로그는 찍히지 않는다.

이 때, DTO에 처리 혹은 Assert 등을 이용한 validtion을 추가하고 싶다면 어떻게 될까?

생성자에 추가해서 처리하면 좋겠지만, 기본 생성자를 이용해서 생성되므로, controller 혹은 service 단에서 처리해야 한다.(물론 validtion의 경우엔 @Valid를 통해 어느정도 처리 가능하다.)

이런 경우, 기본 생성자가 아닌 다른 생성자를 이용해 dto를 바인딩 시키고 싶은 경우 사용 할 수 있는 annotation이 바로 @JsonCreator이다.

이제 다시 코드로 돌아와서, 이 @JsonCreator를 생성자에 선언 해놓고 다시 /test를 요청한다면


아까와 다르게 @Builder 패턴이 적용된 생성자를 통해 바인딩 되어서 “여기타고 생성이 될까요?” 라는 로그가 정상적으로 찍힌 걸 볼 수 있다.


결론

이제 우리는 @RequestBody를 사용할 때 기본 생성자만이 아닌 다른 생성자로도 DTO를 바인딩 할 수 있게 되었으므로, @Valid보다 더 정교한 validtion이나, String을 encoding 해야 할 경우에도 service나 controller가 아닌 생성자에서 처리 할 수 있게 되었으므로 테스트도 더 간편하게 할 수 있게 되었다.

번외로 보통 @JsonCreator는 인스턴스화 할 때 사용 할 생성자를 정의 할 때 사용 하는 annotation으로,
보통 아래 코드처럼 @JsonProperty와 같이 사용해서 엔티티와 완벽히 일치되지 않는 JSON을 역직렬화할 때 주로 사용하는 것 같다.

1
2
3
4
5
6
7
8
9
10
11
12
public class BeanWithCreator {
public int id;
public String name;

@JsonCreator
public BeanWithCreator(
@JsonProperty("id") int id,
@JsonProperty("theName") String name) {
this.id = id;
this.name = name;
}
}

개인적으로 이렇게 쓸 일이 몇 번이나 있을까 싶긴하지만…


전체 코드

참고 사이트

댓글 공유

서론

이전에 작성한 글 중에 @Requestbody Annotation 사용 시 기본 생성자 필요 라는
글을 올린 적이 있는데 글의 마지막에 보면

@Requestbody가 jackson 라이브러리를 이용해 매핑하기 때문이다.

JG.JI@Requestbody Annotation 사용 시 기본 생성자 필요

라는 결론으로 글을 마무리 하였는데, 김영한님의 강의에서 해당 내용과 관련해서 자세하게 알려주시는 부분이 있어, 해당 내용을 정리해본다.


본론

우선 Spring에서는 Controller에서 @RequestBody로 넘어오는 파라미터들에 대해서 내부적으로 ObjectMapper를 이용해 JSON 데이터를 우리가 만든 DTO 객체로 변환시켜주고,
@ResponseBody인 경우엔, DTO 객체를 JSON 데이터로 변환해서 클라이언트에 넘겨준다.

이렇게해서 해소 될 궁금증이었으면 이 글을 읽으러 오지 않았을테니, 해당 내용에 대하여 코드 레벨에서 자세히 알아보기로 한다.

우선, Spring MVC의 동작 순서를 간략하게 서술하자면(구글에 검색하면 훌륭한 분들이 작성하신 훌륭한 글들이 많으므로)

  1. 클라이언트에서 요청이 넘어오면 FrontController 역할을 수행하는 DispatcherServlet에서 먼저 확인해서,
  2. 핸들러 매핑 리스트 중 해당 요청을 처리 할 수 있는 핸들러를 가져오고,
  3. 핸들러 어댑터 리스트 중에서 해당 핸들러를 처리 할 수 있는 핸들러 어댑터를 가져와서,
  4. 해당 어댑터에서 우리가 만든 컨트롤러에 호출해서 비즈니스 로직을 처리 한 후,
  5. 리턴되는 데이터에 맞게 뷰 리졸버나 메시지컨버터가 동작하고,
  6. 알맞게 클라이언트로 반환된다.

여기서, @RequestMapping에 대한 request는 RequestMappingHandlerAdapter에서 처리하는데
이 RequestMappingHandlerAdapter는
1. byte[]를 처리하기 위한 ByteArrayHttpMessageConverter
2. String을 처리하기 위한 StringHttpMessageConverter
3. json을 처리하기 위한 MappingJackson2HttpMessageConverter
등을 가지고 있다.

RequestMappingHandlerAdapter에 설정된 MessageConverter들

RequestMappingHandlerAdapter에 설정된 MessageConverter들

그리고 자신이 가지고 있는 ArgumentResolver들 중
@RequestBody형식으로 들어오는 파라미터를 처리하기 위해 RequestResponseBodyMethodProcessor 라는 ArgumentResolver를 호출한다.

RequestMappingHandlerAdapter에 설정된 ArgumentResolver들


이 RequestResponseBodyMethodProcessor의 readWithMessageConverters 메서드에서 메시지 컨버터 리스트를 loop 돌면서
대상 클래스 타입과 미디어 타입 등을 체크해서 알맞은 메시지 컨버터를 호출해서 사용하는데
이 때 호출되는 MessageConverter가 MappingJackson2HttpMessageConverter라는 MessageConverter이다.

요청을 처리 할 수 있는 MessageConverter 호출

  1. MessageConverter의 canRead() 메서드로 해당 요청을 처리 할 수 있는 컨버터 인지 확인.
  2. 처리 할 수 있으면 read() 메서드로 메시지에서 객체를 읽고 반환한다.

MappingJackson2HttpMessageConverter에서는 최종적으로 readJavaType 메서드가 호출되는데 이 때 objectMapper를 사용해서 json 데이터를 DTO 객체로 변환하는 것을 볼 수 있다.

ObjectMapper를 이용해서 JSON데이터를 변환하는 부분


결론

보이지 않는 곳에서 이렇게 많은 부분을 스프링에서 처리해주고 있었기 때문에 우리는 Controller에서 편하게 파라미터를 사용 할 수 있고, JSON 데이터도 바로 객체로 받을 수 있는 것이었다.
이런 내용을 알아도, 몰라도 결국 같은 방식으로 개발 하겠지만… 언젠가 도움이 되겠거니 생각하고 지금은 궁금증을 해결 했다는 것에 대해 만족한다.
이 글을 읽는 다른 분들도 필자와 같은 궁금증이 있었다면 조금이나마 해소가 됐길 바라며..


참고 사이트

댓글 공유

서론

오늘도 어김없이 영한님의 스프링 MVC 강의를 듣고 있었는데, html form 데이터를 body에 보낼 때는 post 방식 밖에 안된다는 말씀을 하시기에…
‘그럼 GET 방식도 안된다는건가?’ 라는 궁금증이 들어서 직접 확인해본 결과에 대해 작성한다.


본론

우선… 기본적으로 지금 필요한 GET방식과 POST방식의 차이점만을 다시 상기해보자면, GET방식은 데이터가 URL의 query string으로 들어간다는 것과,
POST방식은 데이터가 http message-body에 들어간다는 차이만 알아두면 될 것 같다. (다른 많은 차이가 있지만)

그렇기 때문에 우리가 html form 태그 에서 method attribute에 GET라고 작성한 후 데이터를 submit하면 데이터는 query string에 담겨 서버로 전송되고
POST라고 작성한 후 데이터를 submit하면 데이터는 message-body에 담겨서 서버로 전성되고 이 때 content-type은 application/x-www-form-urlencoded로 전송된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/request-param" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>

그럼 method는 GET이면서 content-type은 application/x-www-form-urlencoded 일 경우엔 어떻게 될까?

postman으로 아래처럼 실행해본 결과 보는 것과 같이 400 error가 응답되며 요청에 실패하였다.

어째서 이런 일이 발생하였나를 코드 레벨에서 확인해보니…

tomcat의 Request class에 있는 parseParameters()라는 메서드에서 넘어온 데이터를 parsing 하는데,
이 때 http method가 post인지 확인하고 post가 아니라면 message-body의 데이터를 파싱 할 수 없게 되어 있는 것을 확인했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
if( !getConnector().isParseBodyMethod(getMethod()) ) {
success = true;
return;
}

...


// 이 아래부턴 Connector.java

protected boolean isParseBodyMethod(String method) {
return parseBodyMethodsSet.contains(method);
}

...

if (null == parseBodyMethodsSet) {
setParseBodyMethods(getParseBodyMethods());
}

...

/**
* Set list of HTTP methods which should allow body parameter
* parsing. This defaults to <code>POST</code>.
*
* @param methods Comma separated list of HTTP method names
*/
public void setParseBodyMethods(String methods) {

HashSet<String> methodSet = new HashSet<>();

if (null != methods) {
methodSet.addAll(Arrays.asList(methods.split("\\s*,\\s*")));
}

if (methodSet.contains("TRACE")) {
throw new IllegalArgumentException(sm.getString("coyoteConnector.parseBodyMethodNoTrace"));
}

this.parseBodyMethods = methods;
this.parseBodyMethodsSet = methodSet;
setProperty("parseBodyMethods", methods);
}

...

/**
* @return the HTTP methods which will support body parameters parsing
*/
public String getParseBodyMethods() {
return this.parseBodyMethods;
}

...


/**
* Comma-separated list of HTTP methods that will be parsed according
* to POST-style rules for application/x-www-form-urlencoded request bodies.
*/
protected String parseBodyMethods = "POST";

위에 정리한 것 처럼

  1. isParseBodyMethod()에서 지금 request의 method가 body를 파싱 할 수 있는 메서드 인지 확인하는데, 이 때 가능한 method들은 인스턴스 변수 parseBodyMethodsSet에 저장되어 있고,
  2. 인스턴스 변수 parseBodyMethodsSet는 setParseBodyMethods() 메서드에서 저장되고 있고~
  3. parseBodyMethodsSet가 null인 경우에
  4. setParseBodyMethods(getParseBodyMethods()) 메서드를 실행해서 이 때 method를 저장 시키는데,
  5. getParseBodyMethods() 메서드를 호출하면
  6. 인스턴스변수 parseBodyMethods를 return 하는데
  7. 이 때 이 인스턴스 변수 parseBodyMethods에는 이미 “POST”로 method가 지정되어 있기 때문에
  8. GET방식 일 경우에는 message-body를 파싱 할 수 있는 method가 아닌 것으로 처리 되므로~
  9. 파싱 할 수 없다는 에러 메시지를 출력하면서 프로세스가 끝이난다.

결론

생각해보면… content-type은 message-body이 어떤 형식의 데이터로 담겨 있는가를 알려주는 일종의 os의 파일 확장자같은 개념인데
당연히 get방식의 경우 message-body에 데이터가 담기는 것이 아니니 get방식으로 content-type만 application/x-www-form-urlencoded로 한다고 될 턱이 없는데
강의를 듣기만 했을 때는 왜 생각하지 못했는 지 모르겠다.. 머리가 나쁘면 몸이 고생이라더니 옛말에 틀린 말이 없다.


참고 사이트

댓글 공유

서론

우선 사과의 말씀부터 올린다. 제목은 ‘spring에서~’ 지만 정확히는 tomcat에서 ~ 로 보는 것이 맞다.

인프런에서 김영한 님의 Spring MVC 강의를 보던 도중 … ‘http request를 정확히 어떠한 방식으로 파싱하고 있을까?’라는 궁금증이 생겨서 강의를 듣다가 말고 삽질을 한 내용을 기록해둔다.


본론

우선, 처음으로 궁금증을 해결하기 위해 들어가본 곳은 http request 요청을 받기 위한 HttpServletRequest interface의 tomcat 구현체인 RequestFacade class부터 시작했다.

이 중 getHeader 메서드에 대해 찾아보면,

Returns the value of the specified request header as a String.
If the request did not include a header of the specified name, this method returns null.
If there are multiple headers with the same name, this method returns the first head in the request.
The header name is case insensitive.
You can use this method with any request header.

라고 설명되어 있고… 이 때 코드에서 request의 header를 살펴보면

이미 header에 정보들이 저장되어 있는 상태

이미지와 같이 이미 header에 대한 정보들이 각각 저정되어 있는 걸 볼 수 있다.

필자가 궁금했던 것은 ‘이 request에 header는 언제 저장되는가?’ 였으므로
막연하게 ‘여기서 더 타고 올라가면 찾아 낼 수 있겠구나..’ 라고 안일한(?) 생각을 하고 안으로 더 들어가보았다.

타고타고 가다보니 …
http 1.1 header에 의한 요청이므로 Http11Processor의 service 메서드에서

1
2
3
4
5
6
7
8
// Don't parse headers for HTTP/0.9
if (!http09 && !inputBuffer.parseHeaders()) {
// We've read part of the request, don't recycle it
// instead associate it with the socket
openSocket = true;
readComplete = false;
break;
}

처럼 작성되어 있는 코드를 발견했고, 여기에 있는 inputBuffer.parseHeaders() 을 보니
이름부터 여기서 http request에 대해 파싱을 할 것 같아 이 안으로 들어가보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
while (headerParsePos == HeaderParsePosition.HEADER_NAME) {

// Read new bytes if needed
if (byteBuffer.position() >= byteBuffer.limit()) {
if (!fill(false)) { // parse header
return HeaderParseStatus.NEED_MORE_DATA;
}
}

int pos = byteBuffer.position();
chr = byteBuffer.get();
if (chr == Constants.COLON) {
headerParsePos = HeaderParsePosition.HEADER_VALUE_START;
headerData.headerValue = headers.addValue(byteBuffer.array(), headerData.start,
pos - headerData.start);
pos = byteBuffer.position();
// Mark the current buffer position
headerData.start = pos;
headerData.realPos = pos;
headerData.lastSignificantChar = pos;
break;
} else if (!HttpParser.isToken(chr)) {
// Non-token characters are illegal in header names
// Parsing continues so the error can be reported in context
headerData.lastSignificantChar = pos;
byteBuffer.position(byteBuffer.position() - 1);
// skipLine() will handle the error
return skipLine();
}

// chr is next byte of header name. Convert to lowercase.
if ((chr >= Constants.A) && (chr <= Constants.Z)) {
byteBuffer.put(pos, (byte) (chr - Constants.LC_OFFSET));
}
}

들어가서보니 해당 코드처럼

현재 header data의 파싱된 위치가 key, value 형식의 데이터인 header의 name 부분 이라면 계속 반복하면서 버퍼에 저장하고 COLON(:) 을 만나게 되면,

1
2
headerData.headerValue = headers.addValue(byteBuffer.array(), headerData.start,
pos - headerData.start);

코드가 실행되면서 아까 봤던 request의 MimeHeaders 타입인 header에 name으로 저장되고

value에도 위와 같은 방식으로 byte 단위로 이동하고 저장되면서 header에 value에 저장된다.


결론

서블릿이 http 통신을 위해 귀찮은 것들을 해준다는 것은 알고 있었지만
실제로 어떻게 하고 지에 대해서는 신경을 안 쓰고 있었는데 이렇게 강의를 듣다 보니 궁금해서 한번 확인해보았다.

만일 이 작업을 서블릿에서 대신 안해주지 않고 개발자가 개발할 때마다 해줘야한다고 생각하면…
머리가 어지러워지는 것 같아 이만 글을 줄인다…


참고 사이트

댓글 공유

Junggu Ji

author.bio


author.job