목차
개요
List 선언 시, 선언과 동시에 초기화하여 사용하기 편리하여 Arrays.asList()를 종종 사용했습니다. 편리하게 사용도중 필요에 의해 선언된 리스트 첫 원소에 새로운 원소를 더해서 사용해야하는 상황이 있어 add 메서드를 사용하였습니다. 컴파일 에러도 없기에 자신있게 소스를 실행했지만 실제 실행 단계에서 다음과 같은 예외가 발생하였습니다. "arrays.aslist unsupportedoperationexception" 이 오류의 원인을 찾던 중 Arrays.asList() 사용하는데 조심해야 할 부분을 찾게 되어 공유하고자 글을 남깁니다.
Arrays.asList()의 결과물은 Arrays 안의 inner 클래스다.
오류가 나는 부분을 디버깅 찍어보면 평소에 사용하는 java.util.ArrayList가 아니라 Arrays$arrayList로 찍히는 것을 볼 수 있습니다. 즉, Arrays안의 inner 클래스 임을 알 수 있습니다.
결론부터 말씀드리자면 아래의 과정으로 인해 unsupportedoperationexception 오류가 발생하게 됩니다.
- Arrays Class(Arrays$arrayList Class)를 살펴보면, AbstractList를 상속받아 생성된 것을 확인할 수 있다.
- abstractList class에 있는 add나 addAll메서드를 override 하고 있지 않다.
- 그렇기 때문에 add()메서드 사용 시, AbstractList Class에 있는 add() 메서드를 수행하게 된다.
- AbstractList Class를 살펴보면 add() 메서드 사용 시 UnsupportedOperationException을 던지게 되어 있다.
아래는 각 코드 입니다.
**평소 사용하는 ArrayListClass
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8683452581122892189L;
...
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
...
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
...
}
** Arrays.asList() 사용 시 사용하게 되는 Class
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size() {
return a.length;
}
@Override
public Object[] toArray() {
return a.clone();
}
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
int size = size();
if (a.length < size)
return Arrays.copyOf(this.a, size,
(Class<? extends T[]>) a.getClass());
System.arraycopy(this.a, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
@Override
public E get(int index) {
return a[index];
}
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
return oldValue;
}
@Override
public int indexOf(Object o) {
E[] a = this.a;
if (o == null) {
for (int i = 0; i < a.length; i++)
if (a[i] == null)
return i;
} else {
for (int i = 0; i < a.length; i++)
if (o.equals(a[i]))
return i;
}
return -1;
}
@Override
public boolean contains(Object o) {
return indexOf(o) != -1;
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(a, Spliterator.ORDERED);
}
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (E e : a) {
action.accept(e);
}
}
@Override
public void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
E[] a = this.a;
for (int i = 0; i < a.length; i++) {
a[i] = operator.apply(a[i]);
}
}
@Override
public void sort(Comparator<? super E> c) {
Arrays.sort(a, c);
}
}
**AbstractList Class
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
protected AbstractList() {
}
...
// 해당 메소드가 실행되는 것이다.
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
...
}
해결법
해결법은 간단합니다.
ArraysList나 LinkedList 등의 컬렉션 클래스로 한번 Wrapping해서 사용하면 됩니다.
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
결론
Arrays.asList()를 사용 시 주의할 점에 대해 알아보았습니다. 해결법이라고 작성한 부분에서 Wrapping해 사용하면 된다고 하였지만 제일 좋은 방법은 자신이 사용하려는 리스트가 고정된 리스트인지 원소에 변경이 있을 리스트인지 구분해서 적절한 초기화 방법을 처음부터 사용하는 것이 좋을 것 같다고 생각합니다.
https://yonguri.tistory.com/137
https://dev-jwblog.tistory.com/72
https://blog.gangnamunni.com/post/Arrays-arrayList-ArrayList/
'Java' 카테고리의 다른 글
List에서 특정 조건 만족하는 요소 삭제하기 (List.removeIf 사용하기) (0) | 2025.01.17 |
---|---|
상속관계의 클래스 일 때, @Builder 사용하기 (0) | 2024.11.16 |
ObjectMapper, Pattern 등의 클래스를 싱글톤으로 사용해도 될지 고민했던 내역 (0) | 2024.11.15 |
인텔리제이에서 자바 버전 변경하기 (0) | 2023.10.17 |
Java에서의 HashMap 동작 원리 파악하기 (0) | 2023.07.27 |
목차
개요
팀 내에서 대규모 데이터에 대해 일정 시간마다 신규 데이터가 인입되는 상황에서 집계 쿼리를 통해 대시보드를 보여주어야 하는 상황일 때, 매 요청마다 집계쿼리를 실행하여 보여주기에는 코스트가 너무 높다고 판단하여 방법을 찾던 중 Materialized View를 사용하기로 결정하였습니다. 이 과정에서 Materialized View라는 내용을 처음 듣게 되어 정리해보았습니다.
Materialized View란?
Materialized View는 데이터베이스에서 사용되는 객체로, 복잡한 쿼리 결과를 저장하고 이를 주기적으로 갱신하여 빠르게 접근할 수 있게 해주는 구조입니다. 기본적으로 View와 비슷하지만, View는 쿼리 결과를 실시간으로 계산해서 반환하는 반면, Materialized View는 결과를 물리적으로 저장해두고 필요할 때 그 데이터를 제공합니다.
특징
1. 쿼리 성능 향상
- Materialized View는 복잡한 계산이 포함된 쿼리의 결과를 저장해두기 때문에, 동일한 쿼리를 다시 실행할 때 물리적인 데이터 저장소에서 바로 데이터를 가져올 수 있어 쿼리 성능이 크게 향상됩니다.
2. 주기적인 갱신
- Materialized View는 저장된 데이터를 주기적으로 갱신할 수 있습니다. 갱신 방법은 전체 갱신(Full Refresh) 방법이 있습니다.
3. 디스크 공간 사용
- Materialized View는 데이터를 저장하는 형태이기 때문에 디스크 공간을 더 사용합니다. 이는 비효율적인 경우가 될 수 있습니다.
4. 실시간 데이터 반영 불가
- Materialized View는 저장된 데이터를 기반으로 작동하기 때문에, View와 달리 실시간 데이터 반영에는 제한이 있습니다. 갱신 주기 내에 변경된 데이터는 반영되지 않기 때문에 주의가 필요합니다.
https://www.alibabacloud.com/blog/598129
https://wiki.postgresql.org/wiki/Incremental_View_Maintenance
'DB' 카테고리의 다른 글
[MariaDB] ELT (랜덤한 값 넣을 때, 사용한 함수) (0) | 2025.01.17 |
---|---|
TDE란? (0) | 2024.11.26 |
DBeaver 설치 및 DB 연동하기 (0) | 2023.11.03 |
목차
개요
PoC를 위한 데이터 구성 중 유용한 함수를 발견하여 정리해보았습니다.
ELT
더미데이터 구성 중 랜덤하게 다양한 값을 넣어야 하는 상황에서 아래와 같은 MariaDB 함수를 발견했습니다.
ELT(N, str[, str2, str3, ...])
2번째 인자 이후에 있는 값들에 대해 N에 해당하는 값을 반환하는 함수 입니다.
아래와 같이 사용하였습니다.
UPDATE testDB
SET test_accuracy = ELT(FLOOR(1 + (RAND() * 4)), 98.5, 92.75, 99.2, 99.99);
참고자료
'DB' 카테고리의 다른 글
Materialized View란? (0) | 2025.02.06 |
---|---|
TDE란? (0) | 2024.11.26 |
DBeaver 설치 및 DB 연동하기 (0) | 2023.11.03 |
목차
개요
List 사용 중에 목록에서 특정 조건을 만족하는 요소를 삭제하는 로직을 다룰 일이 종종 있습니다.
이 때, 그 동안은 매번 반복문을 작성하여 구현하였는데 List 자체 메서드에 좋은 기능이 있다는 것을 알게되어 글을 정리하게 되었습니다.
기존 사용하던 방법
for문을 돌려가며 찾아서 삭제 했었습니다.
import java.util.*;
public class test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
for(int i=0;i<list.size();i++){
String str = list.get(i);
if(str.equals("A")) list.remove(i--);
}
System.out.println(String.join(", ", list));
}
}
위와 같이 리스트 중 A인 원소가 있으면 삭제하라는 방식의 코드였습니다.
해당 코드로 실행하고자하면 A가 삭제 될 때, 리스트의 전체 길이가 주는 것을 고려하여 i를 하나 빼주어야 합니다.
이 때문에 i-1 값을 생각하고 코드를 작성해야해서 가독성이 떨어집니다.
iterator로 하는 방법도 있습니다.
아래의 블로그 글을 참고해주시면 됩니다.
List.removeIf 사용하기
List 자체 함수로 위에서 작성하였던 코드를 바꾸면 아래와 같이 됩니다.
list.removeIf(str -> str.equals("A"));
System.out.println(String.join(", ", list));
매우 간결하며 직관적으로 변경되었음을 볼 수 있습니다.
참고자료
'Java' 카테고리의 다른 글
Arrays.asList() 사용 시 주의할 점 (0) | 2025.04.03 |
---|---|
상속관계의 클래스 일 때, @Builder 사용하기 (0) | 2024.11.16 |
ObjectMapper, Pattern 등의 클래스를 싱글톤으로 사용해도 될지 고민했던 내역 (0) | 2024.11.15 |
인텔리제이에서 자바 버전 변경하기 (0) | 2023.10.17 |
Java에서의 HashMap 동작 원리 파악하기 (0) | 2023.07.27 |
목차
개요
프로젝트 중 대용량 데이터를 프론트엔드에 보내야 할 일이 있었습니다. 이 과정에서 전체를 한 번에 보내려고 하니 백엔드에도 무리가 가고 통신에도 무리가 가는 것으로 보여 청크 단위로 통신하여 전달하는 방식을 고안하고자 ResponseBodyEmitter를 찾게 되었습니다. 우선 더미로 테스트 코드를 진행해보았습니다.
시작하기 앞서
ResponseBodyEmitter를 쓰려면 비동기가 작동 가능하도록 하는 것이 필수입니다. 저는 이미 비동기 작업을 위해 @EnableAsync를 해줬는데 ResponseBodyEmitter를 쓰려고 하니 아래와 같은 에러 로그가 발생하였습니다.
java.lang.IllegalStateException: Async support must be enabled on a servlet and for all filters involved in async request processing. This is done in Java code using the Servlet API or by adding "true" to servlet and filter declarations in web.xml.
at org.springframework.util.Assert.state(Assert.java:392)
at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.startAsync(StandardServletAsyncWebRequest.java:103)
at org.springframework.web.context.request.async.WebAsyncManager.startAsyncProcessing(WebAsyncManager.java:428)
at org.springframework.web.context.request.async.WebAsyncManager.startDeferredResultProcessing(WebAsyncManager.java:408)
at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler.handleReturnValue(ResponseBodyEmitterReturnValueHandler.java:159)
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:81)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:130)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:489)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:583)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:212)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:156)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:181)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:156)
at cohttp://m.duzon.common.filter.P3PFilter.doFilter(P3PFilter.java:22)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:181)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:156)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:181)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:156)
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:89)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:181)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:156)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:168)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:130)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:679)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:617)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:934)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1698)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
확인해본 결과 @EnableAsync를 통한 비동기 가능하도록 하는 설정은 스프링 자체에서 비동기 사용 가능하게 해주는 것이고 ResponseBodyEmitter는 Servlet까지 나가기에 별도의 설정이 추가적으로 필요하다고 합니다.
(참고사항 : 일부 구형 서블릿 컨테이너(Tomcat6 또는 그 이전 버전)에서는 비동기 처리가 지원되지 않습니다. Servlet 3.0 이상이 필요합니다.)
저희는 환경 설정을 xml 파일로 진행하고 있었기에 web.xml 파일의 <filter>태그와 <servlet>태그에
<async-supported>true</async-supported>
를 넣어주어 해당 오류를 해결하였습니다.
간단하게 적용하기
단순히 ResponseBodyEmitter를 사용해본다는 수준으로 한 번 테스트 해보았습니다.
백엔드 코드 입니다.
Controller에서 바로 동작을 진행합니다.
@GetMapping("/test-emitter")
public ResponseBodyEmitter getTestEmitter() throws IOException {
ResponseBodyEmitter responseBodyEmitter = new ResponseBodyEmitter();
Executors.newSingleThreadExecutor().submit(() -> {
try {
for (int i=0; i<10; i++) {
responseBodyEmitter.send("test\n");
Thread.sleep(1000);
}
responseBodyEmitter.complete();
} catch (Exception e) {
responseBodyEmitter.completeWithError(e);
}
});
return responseBodyEmitter;
}
ResponsBodyEmitter 객체를 생성하고 해당 객체를 별도의 스레드 위에서 실행시키도록 해줍니다.
예시에서는 Executors.newSingleThreadExecutor()로 별도의 실행 스레드를 만들어주었지만 실제 프로젝트에 적용하고자 하면 청크 통신을 위한 별도의 ThreadPool을 생성하여 작동하도록 해야 할 것 같습니다.
이후, 별도의 스레드에서 responseBodyEmitter에 메시지를 보내면 1초에 한 번씩 메시지가 프론트엔드에 가는 것을 볼 수 있을 것 입니다. 참고로 send 메서드 실행 시, 보내는 메시지의 형태를 정할 수 있습니다. TEXT, JSON 형식 등을 말이죠.
(예시)
responseBodyEmitter.send(hashMap, MediaType.APPLICATION_JSON);
해당 값이 제대로 오는지 확인하기 위해 만약 포스트맨을 쓴다고 하면 제대로 동작하지 않을 것 입니다. 포스트맨은 청크 통신을 지원하지 않는다고 하더군요.
직접 터미널에서 호출하여 테스트하거나 간단한 프론트엔드 코드를 만들어 테스트하면 됩니다.
테스트를 위한 프론트엔드 코드
아래의 간단한 프론트엔드 코드를 만들어 테스트를 진행해보았습니다.
import React, { useState } from 'react';
function App() {
const [messages, setMessages] = useState([]);
const [error, setError] = useState(null);
const handleClick = async () => {
setMessages([]); // Clear previous messages
setError(null); // Clear previous errors
try {
const response = await fetch('/tmp/test-emitter');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
while (true) {
const { done, value } = await reader.read();
if (done) break; // End of stream
const chunk = decoder.decode(value);
setMessages((prev) => [...prev, chunk]);
}
} catch (err) {
setError(`Error: ${err.message}`);
}
};
return (
<div style={{ padding: '20px' }}>
<h1>Test Emitter</h1>
<button onClick={handleClick} style={{ marginBottom: '20px' }}>
동작하기
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
<div style={{ background: '#f4f4f4', padding: '10px', borderRadius: '5px' }}>
{messages.length > 0 ? (
messages.map((msg, index) => <p key={index}>{msg}</p>)
) : (
<p>결과가 여기에 표시됩니다.</p>
)}
</div>
</div>
);
}
export default App;
아래와 같은 결과가 나오는 것을 확인 할 수 있었습니다.
추후 진행하려고 하는 사항들
JSON을 연달아 보내면 프론트엔드가 어떻게 처리할까 궁금하여 JSON 형태로 보내는 연습을 해보려고 합니다.
또한 현재는 간단한 구조라 Controller에서 진행을 했는데 (대부분의 예시도 Controller에서 끝) DB 조회도 나누어 진행하고 보낼 수 있는지 궁금하여 진행해보려고 합니다.
마지막으로 실제 프로젝트에서는 다른 서버에서 응답을 우리 서버로 받아온 다음 프론트로 넘겨야 하는데 Spring으로 청크 데이터를 받아 프론트엔드로 보내는 연습도 필요할 것으로 보입니다.
내가 하고 싶었던 최종 형태
백엔드 코드
public void responseBodyEmitterTest(ResponseBodyEmitter responseBodyEmitter) throws IOException {
String[][] strs = {{"columnA|columnB|columnC|columnD|columnE", "a1|b1|c1|d1|e1", "a2|b2|c2|d2|e2"},
{"a3|b3|c3|d3|e3", "a4|b4|c4|d4|e4", "a5|b5|c5|d5|e5"},
{"a6|b6|c6|d6|e6", "a7|b7|c7|d7|e7", "a8|b8|c8|d8|e8"}};
Executors.newSingleThreadExecutor().submit(() -> {
try {
for (int i=0; i<3; i++) {
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("value", strs[i]);
responseBodyEmitter.send(hashMap, MediaType.APPLICATION_JSON);
Thread.sleep(1000);
}
responseBodyEmitter.complete();
} catch (Exception e) {
responseBodyEmitter.completeWithError(e);
}
});
}
프론트엔드 코드
참고자료
https://m.blog.naver.com/jdjhaha/222169740040
'Spring' 카테고리의 다른 글
Spring Boot에서 PostgreSQL 연동하기 (0) | 2023.10.28 |
---|
목차
개요
node 16을 설치해서 사용 할 일이 있어 brew install node@16을 했는데 아래와 같은 에러 메시지를 받았습니다.
Error: node@16 has been disabled because it is not supported upstream! It was disabled on 2024-11-03.
꼭 node 16을 설치해야 했기에 다른 방법을 이용해서라도 설치가 필요해 시도한 방법을 정리해보았습니다.
해결방법
nvm을 이용해 설치하도록 하여 해결하였습니다.
- nvm 설치
brew install nvm
- 설치 후 .zshrc 또는 .bashrc 파일에 다음 줄을 추가
export NVM_DIR="$HOME/.nvm"
[ -s "$(brew --prefix nvm)/nvm.sh" ] && \. "$(brew --prefix nvm)/nvm.sh"
변경 사항을 저장하려면 터미널을 다시 시작하세요.
- Node.js 16 설치
nvm install 16
목차
개요
업무를 하다보니 TDE라는 용어를 듣게 되었습니다. 해당 내용이 무엇인지 궁금하여 찾아보았습니다.
TDE란?
TDE(Transparent Data Encryption)는 데이터베이스에서 저장된 데이터를 암호화하여 보안을 강화하는 기술입니다. 주요 특징은 데이터베이스 애플리케이션이나 쿼리 수정 없이 데이터 암호화를 적용할 수 있다는 점입니다.
TDE의 작동 방식
- 암호화 대상: 디스크에 저장되는 데이터베이스 파일, 로그 파일 등.
- 데이터를 디스크에 기록할 때 암호화하고, 메모리에서 읽을 때 자동으로 복호화합니다.
- 투명성: 애플리케이션에서 접근하는 데이터는 평문(복호화된 상태)으로 제공되므로 추가적인 코딩 작업이 필요하지 않습니다.
TDE의 주요 구성 요소
- 데이터 암호화 키(DEK, Data Encryption Key)
- 데이터를 암호화하는 데 사용됩니다.
- 보통 고성능을 위해 대칭 키 암호화 방식을 사용합니다.
- DEK는 별도의 **마스터 키(Master Key)**로 암호화되어 보호됩니다.
- 마스터 키(Master Key)
- 데이터베이스 외부(예: 하드웨어 보안 모듈(HSM) 또는 운영체제의 키 스토리지)에 저장되어 DEK를 보호합니다.
TDE의 주요 장점
- 데이터 보호
- 데이터베이스 파일이 탈취당하거나 디스크를 분실하더라도 암호화된 상태이므로 읽을 수 없습니다.
- 애플리케이션 변경 불필요
- 암호화/복호화 과정이 투명하게 처리되므로 기존 애플리케이션 로직 수정이 필요 없습니다.
- 법적 및 규제 준수
- PCI-DSS, GDPR 같은 규제에서 요구하는 데이터 보호 요건 충족 가능.
TDE의 한계
- 성능 저하
- 암호화/복호화 작업으로 인해 I/O 성능에 영향을 줄 수 있습니다.
- 전송 중 데이터 보안 미포함
- TDE는 디스크에 저장된 데이터를 보호하지만, 네트워크를 통해 전송되는 데이터는 별도의 암호화(예: TLS)가 필요합니다.
- 키 관리 중요성
- 마스터 키가 유출되거나 분실되면 암호화된 데이터를 복구할 수 없습니다.
주요 데이터베이스에서 TDE 지원 여부
- Oracle: Oracle Advanced Security 옵션으로 TDE 제공.
- Microsoft SQL Server: TDE 기본 지원.
- MySQL/MariaDB: MySQL 5.7.12 이상 및 MariaDB 10.1 이상에서 TDE 지원.
- PostgreSQL: TDE는 기본적으로 제공되지 않지만, 확장 모듈이나 파일 시스템 암호화를 통해 구현 가능.
DBMS 자체 제공 암호화 제품(TDE)의 보안 문제점 분석한 블로그 글도 있어 함께 읽어보면 좋을 것 같습니다.
https://m.blog.naver.com/hanajava/223158513771
DBMS 자체 제공 암호화 제품(TDE)의 보안 문제점 분석
DBMS 자체제공 암호화 제품(TDE)의 보안 문제점 분석 저자:조돈섭 * (현) 이글로벌시스템 재직 * 20...
blog.naver.com
MariaDB에 TDE 적용한 케이스
https://ongamedev.tistory.com/560
MariaDB TDE 적용
Transparent Data Encryption - 데이터 저장 시 암호화해서 저장합니다. (공식문서 링크) 원하는 Table만 암호화 할 수도 있고 테이블 마다 다른 암호화 키를 사용할 수 있으며 DB 전체, 그리고 로그도 선택
ongamedev.tistory.com
'DB' 카테고리의 다른 글
Materialized View란? (0) | 2025.02.06 |
---|---|
[MariaDB] ELT (랜덤한 값 넣을 때, 사용한 함수) (0) | 2025.01.17 |
DBeaver 설치 및 DB 연동하기 (0) | 2023.11.03 |
목차
개요
코드 구현 중, 상속 관계의 클래스에 @Builder를 적용해야 하는 일이 있었습니다. 평소처럼 @Builder 사용하였지만 상속받은 필드는 빌더의 요소로 나타나지 않는 이슈가 있어 관련 내용을 찾게 되었습니다.
@SuperBuilder를 사용하면 된다.
@SuperBuilder는 @Builder 사용 때, 상속받은 필드를 빌더에서 사용하지 못하는 등의 제한을 해결하고자 만들어졌습니다. @SuperBuilder를 사용하면 상속받은 필드도 빌더에서 사용 할 수 있게 됩니다.
주의 할 점은 부모와 자식 클래스 양쪽 모두 @SuperBuilder 어노테이션을 추가해주어야 한다는 것 입니다.
@SuperBuilder 사용 예시
- 부모 클래스 Parent
@SuperBuilder
public class Parent {
private String parentField;
}
- 자식 클래스 Child
@SuperBuilder
public class Child extends Parent {
private String childField;
}
- 최종 예제 코드
Child child = Child.builder()
.parentField("parent")
.childField("child")
.build();
'Java' 카테고리의 다른 글
Arrays.asList() 사용 시 주의할 점 (0) | 2025.04.03 |
---|---|
List에서 특정 조건 만족하는 요소 삭제하기 (List.removeIf 사용하기) (0) | 2025.01.17 |
ObjectMapper, Pattern 등의 클래스를 싱글톤으로 사용해도 될지 고민했던 내역 (0) | 2024.11.15 |
인텔리제이에서 자바 버전 변경하기 (0) | 2023.10.17 |
Java에서의 HashMap 동작 원리 파악하기 (0) | 2023.07.27 |
목차
개요
코드 짤 때, 급하게 짠다고 다른 사람 코드를 복붙하거나 습관적으로 ObjectMapper, Formatter 등을 로컬 메서드에서 매번 새롭게 선언해가며 사용하였었습니다. 이 때, ObjectMapper, Formatter를 싱글톤으로 빈 주입하여 사용하려고 하는데 가능한건가 싶어 확인하다가 찾은 내용을 정리하고자 합니다.
문서에 다 적혀 있음
ObjectMapper, Pattern, DateTimeFormatter를 싱글톤으로 사용해도 괜찮은 이유가 설명 문서에 다 있었습니다.
[ObjectMapper]
[Pattern]
[DateTimeFormatter]
'Java' 카테고리의 다른 글
Arrays.asList() 사용 시 주의할 점 (0) | 2025.04.03 |
---|---|
List에서 특정 조건 만족하는 요소 삭제하기 (List.removeIf 사용하기) (0) | 2025.01.17 |
상속관계의 클래스 일 때, @Builder 사용하기 (0) | 2024.11.16 |
인텔리제이에서 자바 버전 변경하기 (0) | 2023.10.17 |
Java에서의 HashMap 동작 원리 파악하기 (0) | 2023.07.27 |
목차
개요
회사 코드에서 Spring Boot없는 순수 Spring으로 xml 환경 설정을 진행하고 있습니다. 이 때, 평소에 Spring Boot를 쓰던 상황에 편하게 적용하던 테스트 클래스 Component 적용을 Spring 환경에서 어떻게 진행하는지 삽질한 내용을 정리해두고자 합니다.
적용법
적용법은 하단의 블로그를 참고했습니다.
https://codevang.tistory.com/259
jUnit, Spring-Test 라이브러리 사용법
[ jUnit ] 전체 프로젝트(특히 WAS)를 구동하지 않고 단위 코드 테스트를 할 수 있게 해주는 라이브러리 [ Spring-Test ] jUnit을 확장한 스프링의 테스트 라이브러리 스프링 MVC 프로젝트를 진행할 때 코
codevang.tistory.com
테스트 폴더에 설정값을 매번 붙여넣기도 번거로울 것 같아 locations를 사용하고 file: 을 붙여주어 full-path로 입력해 실제 개발 상황에 사용 중인 설정값을 읽어오도록 하였습니다. 다만 저희는 각 상황에 따라 설정파일을 다르게 관리하고 있었기에 아래 블로그 예시와 같이 여러 파일 값을 사용하도록 하였습니다. 또한 매번 입력하는 것이 번거로울 것 같아 블로그에 소개해준 내용처럼 상위 클래스에 설정값을 넣고 상속받아 사용하는 방법을 이용하고자 합니다.
@ContextConfiguration
(locations = {"file:src/main/webapp/WEB-INF/spring/root-context.xml",
"file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml"})
이 방법대로 그대로 잘되었으면 좋았겠지만 2 가지 오류를 만났습니다.
Spring Bean 초기화 중 EL (Exprssion Language) 관련 종속성 누락
제가 만들어둔 설정 Bean들 중 @Valid를 사용하기 위해 만든 MehodValidationConfig 클래스가 있었습니다.
public class MethodValidationConfig {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
return new LocalValidatorFactoryBean();
}
}
테스트 코드 실행 시 methodValidationPostProcessor에 에러가 발생하였다고 로그가 떴었습니다.
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'methodValidationPostProcessor' defined in com.duzon.lulu.dataprotection.config.MethodValidationConfig: Invocation of init method failed; nested exception is javax.validation.ValidationException: HV000183: Unable to initialize 'javax.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead
에러 원인은 Spring에서 메서드 유효성 검증 (Bean Validation)을 수행할 때, javax.el.ExpressionFactory가 필요하지만 클래스패스에 EL 라이브러리가 없어 초기화에 실패한 것이라고 하여 아래와 같이 javax.el 라이브러리를 추가해 해결하였습니다.
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.0</version>
</dependency>
javax.el의 역할은 아래과 같다고 합니다.
javax.el.ExpressionFactory는 유효성 검증 어노테이션 내에서 사용되는 표현식을 평가하는 기능을 제공합니다. 예를 들어, @Size(min = "${minSize}", max = "${maxSize}")와 같은 유효성 표현식이 있는 경우, 이 값을 동적으로 평가하려면 javax.el 라이브러리가 필요합니다. methodValidationPostProcessor는 이러한 표현식을 처리하고 유효성을 동적으로 검증할 수 있도록 설정합니다.
회사 내부 라이브러리 Bean 생성 오류
현재 재직 중인 회사에서는 내부에서 공용 라이브러리를 사용하도록 되어 있습니다. 이 Bean들을 생성하는데 아래와 같은 오류가 발생했습니다.
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name '********': Unsatisfied dependency expressed through field '*******'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name '*******': Unsatisfied dependency expressed through field '*****'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean found for dependency [javax.servlet.http.HttpServletRequest]: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
회사 내부 코드에서는 HttpServletRequest를 Autowired해서 사용하는 경우가 꽤 있는데 이 부분 때문에 오류가 발생한 것으로 보입니다.
HttpServletRequest Bean을 자동 주입하려고 하는데 HTTP 요청을 처리할 수 있는 웹 컨텍스트가 없어서 발생하는 문제입니다. 테스트 환경에서 웹 컨텍스트를 명시적으로 설정하면 해결되는 문제라고 합니다.
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
public class YourTestClass {
// Test methods here
}
각 어노테이션의 의미는 아래와 같다고 합니다.
1. @ExtendWith (SpringExtension.class)
- 역할: Spring의 테스트 컨텍스트를 JUnit 5에서 사용할 수 있게 해주는 확장 클래스입니다.
- 설명: 이 어노테이션을 통해 Spring 컨텍스트의 기본 기능을 테스트에 통합할 수 있습니다. @Autowired와 같은 의존성 주입, 트랜잭션 관리, @Transactional 테스트, 애플리케이션 컨텍스트 로딩 등을 지원합니다.
- 사용법: @ExtendWith(SpringExtension.class)는 기본적으로 모든 Spring 통합 테스트에 포함되어야 하며, Spring의 다양한 테스트 어노테이션과 함께 사용됩니다.
2. @WebAppConfiguration
- 역할: Spring MVC 테스트 시 WebApplicationContext (웹 애플리케이션 컨텍스트)를 로딩하여 웹 관련 빈들을 사용할 수 있도록 설정합니다.
- 설명: Spring MVC 애플리케이션의 웹 환경을 구성하는 WebApplicationContext를 로드하도록 Spring 테스트 컨텍스트에 지시합니다. 이를 통해 MockMvc와 같은 MVC 테스트 기능을 사용하거나 HttpServletRequest 같은 웹 관련 Bean을 사용할 수 있게 됩니다.
- 사용법: @WebAppConfiguration은 일반적으로 웹 애플리케이션 관련 Bean이 필요한 통합 테스트에 추가하며, Spring MVC 컨트롤러와 서비스 계층 간의 상호 작용을 테스트하는 데 유용합니다.
'Spring > Testing' 카테고리의 다른 글
Spring Boot Repository Test 간단하게 실습 (0) | 2023.10.17 |
---|