목차
🧩 M4 Mac에서 gRPC 빌드 오류 해결기
protoc 실행 문제부터 Rosetta 설치까지
🏁 개요
Mac 환경에서 gRPC를 테스트하고 빌드하던 중 여러 오류가 발생했습니다.
이 글에서는 protoc 실행 오류와 CPU 아키텍처 문제를 해결한 과정을 정리합니다.
🚨 1. 오류 발생
gRPC를 설정하고 빌드하는 과정에서 다음과 같은 오류가 발생했습니다.
protoc-gen-grpc-java-1.65.0-osx-aarch_64.exe: program not found or is not executable
문제의 원인은 protoc가 Windows용 .exe 형식으로 설치되어 Mac에서 실행되지 않은 것이었습니다.
🔍 2. 원인 및 해결 과정
검색 결과, grpc-java 공식 이슈 트래커에서 동일한 사례를 발견했습니다.
문제의 핵심은 protoc를 불러올 때 운영체제(OS)와 아키텍처(classifier)가 명시되지 않아
잘못된 바이너리가 다운로드된 것이었습니다.
✅ 해결 방법
Gradle 설정에서 classifier를 명시해 Mac용 바이너리를 다운로드하도록 수정합니다.
protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.14.0:osx-x86_64' } }
이 설정을 적용하면 Mac 환경에 맞는 protoc 파일이 다운로드되며,
이전의 “program not found” 오류가 해결됩니다.
⚠️ 3. 추가 문제 — Bad CPU type in executable
빌드 오류는 사라졌지만, 다음과 같은 새로운 오류가 발생했습니다.
error=86, Bad CPU type in executable
이 오류는 인텔(x86_64)용 바이너리를 ARM(M계열) 기반 Mac에서 실행할 때 발생합니다.
즉, osx-x86_64 바이너리를 M4 Mac에서 실행할 수 없다는 뜻입니다.
🧠 4. 최종 해결 — Rosetta 설치
ARM Mac에서 인텔용 실행 파일을 구동하려면 Rosetta 2가 필요합니다.
아래 명령어로 간단히 설치할 수 있습니다.
softwareupdate --install-rosetta
설치 후 다시 빌드를 실행하면 정상적으로 동작합니다 ✅
🧾 정리
| 구분 | 내용 |
|---|---|
| 문제 원인 | 잘못된 플랫폼용 protoc 바이너리 다운로드 |
| 해결 방법 | Gradle 설정에서 classifier 명시 (osx-x86_64, osx-aarch_64 등) |
| 추가 문제 | ARM 기반 Mac에서 인텔 바이너리 실행 불가 |
| 최종 해결 | Rosetta 2 설치 후 재빌드 성공 |
💡 팁
- M계열 Mac 사용자는
:osx-aarch_64classifier를 사용해야 합니다. protoc버전은grpc-java버전에 맞춰 관리하는 것이 좋습니다.- 관련 공식 문서: gRPC Java GitHub Issues
📘 요약
protoc실행 오류 → classifier 명시로 해결
CPU 타입 오류 → Rosetta 설치로 해결
참조문서
목차
개요
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 |
