2020.12.13 - [프로그래밍언어/JAVA] - Thread

 

<JAVA>Thread

Os 수업을 들었다면 알거라고 생각하지만 한번더 정리하는 개념으로 Process 실행중인 프로그램OS로부터 메모리르 할당 받음 Thread실제 프로그램이 수행되는 작업의 최소 단위하나의 프로세스에는

kwaksh2319.tistory.com

스레드 개념을 한번쯤 보면 좋을것같습니다 ㅎ 둘은 좀 다르지만 일단 둘다 비동기 처리입니다.(기초가 중요하다!)

 

@Async 어노테이션은 주로 Spring Framework에서 사용되며, 메서드를 비동기적으로 실행할 수 있도록 해줍니다. 이를 통해 긴 작업이나 I/O 작업 등을 별도의 스레드에서 실행하여, 호출하는 쪽에서는 즉시 다음 작업을 진행할 수 있게 됩니다.

 

비동기 처리 활성화:
@Async를 사용하기 전에, 애플리케이션 설정에 @EnableAsync를 추가하여 비동기 처리를 활성화해야 합니다.

@Configuration
@EnableAsync
public class AsyncConfig {
    // Executor 설정 등 추가 설정 가능
}

 

메서드에 적용:
비동기 처리가 필요한 메서드 위에 @Async 어노테이션을 붙입니다. 이 메서드는 호출한 스레드와 별도로, 스레드 풀에서 실행됩니다.

@Service
public class MyService {

    @Async
    public void executeAsyncTask() {
        // 시간이 오래 걸리는 작업
    }
}

 

반환 타입:

  • void: 결과를 기다리지 않을 경우 사용합니다.
  • Future / CompletableFuture: 비동기 작업의 결과를 받아보고 싶을 때 사용합니다.
@Async
public CompletableFuture<String> asyncMethodWithReturn() {
    // 처리 로직
    return CompletableFuture.completedFuture("완료");
}

 

주의 사항:

  • public 메서드여야 프록시를 통한 AOP 적용이 제대로 이루어집니다.
  • 같은 클래스 내부에서 호출하는 경우에는 프록시가 적용되지 않아 비동기 처리가 동작하지 않을 수 있으므로, 별도의 빈으로 분리하여 호출하는 것이 좋습니다.
  • 스레드 풀 설정을 커스터마이징하여, 애플리케이션에 맞는 스레드 수와 동작 방식을 조정할 수 있습니다.

주의사항

  • 스레드 관리: 비동기 처리 시에는 스레드 풀이나 이벤트 루프 등을 적절하게 설정해, 자원 누수나 과도한 스레드 생성 문제를 피해야 합니다.
  • 동시성 이슈: 여러 스레드에서 동시에 데이터에 접근할 경우 동기화 문제가 발생할 수 있으므로, 상태 관리에 주의해야 합니다.
  • 프레임워크 선택: Java에서는 Netty, Spring WebSocket, Reactor 등 비동기 처리를 지원하는 프레임워크를 활용하면 편리합니다.

간단히 알아봤는데요. 음 뭐 스레드 사용해서 비동기 처리하는 부분인데요. 이론은 알아도 개발하면서 이해하는 것 만큼 좋은것 없는것같습니다. 

다음에 소켓통신을 통해서 개발한 부분을 비동기 처리를 해보도록 하겠습니다. ㅎㅎ ( 실은 회사에서 업무할때 자동화 시스템 추가 개발하는데 async를 써보는데 파일업로드가 비동기 처리가 안되더라고요 , 이유가  파일 업로드의 경우 클라이언트와의 연결이 파일 전송 완료까지 유지되어야 하고, 해당 요청 자체가 블로킹 I/O를 필요로 하므로, 비동기 처리의 효과가 제한이 되더라거요 ㅎ 사실 자주 안써보다보니 이번에 같이 공부하면서 자세히 다뤄보도록 하겠습니다. )

' > Spring' 카테고리의 다른 글

@Builder? 와 Bulder 패턴?  (0) 2025.02.12
Reflection api GetMethod  (0) 2024.04.20
Spring Data JPA - 구성  (0) 2024.02.25
Spring Data JPA- repository 인터페이스의 정의  (0) 2024.02.18
Spring Data JPA - 핵심 개념  (0) 2024.02.03

1. Builder 패턴이란?

Builder 패턴은 복잡한 객체의 생성 과정을 단순화하고 가독성을 높이기 위해 사용되는 생성 패턴(Creational Pattern) 중 하나입니다. 특히 매개변수가 많은 클래스를 생성할 때, 생성자 또는 정적 팩토리 메서드보다 더 직관적이고 명확한 방법을 제공합니다.

 

2. Builder 패턴의 장점

  1. 가독성 향상: 어떤 필드가 어떤 값으로 설정되는지 명확하게 보입니다.
  2. 불변성 유지: 객체 생성 후 필드를 변경할 수 없게 하여 불변 객체를 쉽게 만들 수 있습니다.
  3. 유연한 객체 생성: 필수 매개변수와 선택 매개변수를 구분하여 유연하게 객체를 생성할 수 있습니다.
  4. 코드 중복 감소: 여러 생성자 오버로딩 없이 다양한 조합으로 객체를 생성할 수 있습니다.

3. 전통적인 Builder 패턴 예제

public class User {
    private final String firstName;
    private final String lastName;
    private final int age;
    private final String email;

    // private 생성자
    private User(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.email = builder.email;
    }

    // static nested Builder 클래스
    public static class Builder {
        private String firstName;
        private String lastName;
        private int age;
        private String email;

        public Builder firstName(String firstName) {
            this.firstName = firstName;
            return this;
        }

        public Builder lastName(String lastName) {
            this.lastName = lastName;
            return this;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        // 최종적으로 객체를 생성하는 build 메서드
        public User build() {
            return new User(this);
        }
    }

    @Override
    public String toString() {
        return "User{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                '}';
    }
}

// 객체 생성 예제
public class Main {
    public static void main(String[] args) {
        User user = new User.Builder()
                .firstName("John")
                .lastName("Doe")
                .age(30)
                .email("john.doe@example.com")
                .build();

        System.out.println(user);
    }
}
User{firstName='John', lastName='Doe', age=30, email='john.doe@example.com'}

4. Lombok의 @Builder 어노테이션 사용

Lombok은 자바에서 보일러플레이트 코드를 줄이기 위해 사용하는 라이브러리입니다. @Builder 어노테이션을 사용하면 위와 같은 Builder 패턴을 자동으로 생성해줍니다.

Lombok 사용 예제:

 

import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
public class User {
    private String firstName;
    private String lastName;
    private int age;
    private String email;
}

public class Main {
    public static void main(String[] args) {
        User user = User.builder()
                .firstName("John")
                .lastName("Doe")
                .age(30)
                .email("john.doe@example.com")
                .build();

        System.out.println(user);
    }
}
User(firstName=John, lastName=Doe, age=30, email=john.doe@example.com)

5. 전통적인 Builder 패턴 vs Lombok @Builder

항목전통적인 Builder 패턴Lombok @Builder 사용

코드 길이 길고 보일러플레이트 코드가 많음 코드가 간결하고 깔끔함
유연성 직접 커스터마이징 가능 기본적인 기능은 자동 생성되나, 커스터마이징은 제한적임
의존성 추가 라이브러리 필요 없음 Lombok 라이브러리 필요
가독성 및 유지보수 가독성이 좋지만 코드가 길어짐 가독성과 유지보수 모두 용이

6. 결론

  • 전통적인 Builder 패턴은 커스터마이징이 필요하거나, 라이브러리 의존성을 피하고 싶은 경우 유용합니다.
  • Lombok의 @Builder는 코드의 가독성과 개발 생산성을 높여주며, 특히 간단한 객체 생성을 빠르게 처리할 수 있습니다.

' > Spring' 카테고리의 다른 글

Async 어노테이션 ,@Async 비동기 처리  (0) 2025.02.19
Reflection api GetMethod  (0) 2024.04.20
Spring Data JPA - 구성  (0) 2024.02.25
Spring Data JPA- repository 인터페이스의 정의  (0) 2024.02.18
Spring Data JPA - 핵심 개념  (0) 2024.02.03

https://kwaksh2319.tistory.com/1004

웹소켓통신 서버 이용하여 간단한 채팅방 만들기2

https://kwaksh2319.tistory.com/1003 웹소켓통신 서버 이용하여 간단한 채팅방 만들기1구조 : STOMPSTOMP은 **"Simple (or Streaming) Text Oriented Messaging Protocol"**의 약자입니다. 웹소켓(WebSocket)을 사용할 때 메시지를

kwaksh2319.tistory.com


이번에 조금 변경해서 ui/ux 조금 수정을 하고 채팅방까지 구현했습니다. 
그리고 채팅로그가 남도록했습니다. 화면있고 코드도 간단히 설명하겠습니다.
화면: 

메세지를 보내면 저장하도록 변경했습니다. 

 @Override
    public void sendMessage(@DestinationVariable Long roomId, MessageDTO message, StompHeaderAccessor headerAccessor) {
        String clientIp = (String) headerAccessor.getSessionAttributes().get("ipAddress");
        if (clientIp == null) {
            clientIp = "unknown"; // 기본값 설정
        }

        try{
            Optional<Room> room =roomService.findById(roomId);
            if(room.isPresent()){
                Message messageEntity = Message.builder()
                        .room(room.get())
                        .sender(message.getSender())
                        .text(message.getContent())
                        .ip(clientIp)
                        .build();
                messageRepository.save(messageEntity);

                String destination = "/topic/room/" + roomId; // 방 ID 경로

                messagingTemplate.convertAndSend(destination, message); // 해당 방의 사용자들에게 메시지 전송

            } else {
            // 예외를 던지면, 아래 @MessageExceptionHandler가 처리합니다.
            throw new IllegalArgumentException("메세지를 보낼 수 없습니다. 올바른 방이 아닙니다.");
        }

        }catch (Exception e){
            log.error("message table save error");
        }

   }

 
그리고 방을 접속할때 메세지들을 가져옵니다. 

 @Override
    public Page<MessageDTO> findAll(@Param("roomId") Long roomId) {
        int pageSize=100;
        int pageNumber=0;
        Page<MessageDTO> messageDTOPage=null;
        try {

            Page<Message> messages =messageRepository.findAll(roomId,PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").ascending()));

             messageDTOPage = messages.map(message ->
                    MessageDTO.builder()
                            .content(message.getText())
                            .sender(message.getSender())
                            .build());
        }catch (Exception e){
            log.error("message  table get content error or roomid not convert string");
        }

        return messageDTOPage;
    }

 

https://kwaksh2319.tistory.com/1003

웹소켓통신 서버 이용하여 간단한 채팅방 만들기1

구조 : STOMPSTOMP은 **"Simple (or Streaming) Text Oriented Messaging Protocol"**의 약자입니다. 웹소켓(WebSocket)을 사용할 때 메시지를 주고받기 위한 프로토콜 중 하나로, 텍스트 기반의 메시징 프로토콜입니다.

kwaksh2319.tistory.com


 
지난번에 이어서~ 
테이블 설계도 조금 변경했습니다. ( 퇴근 후 개발하는건 늘 쉽지 않네요 ~ , 퇴근후 딴짓을 너무 많이하네요 ㅠ ㅠ )
간단하게 방리스트들을 만들었습니다.
 
화면 : 

1:n 관계로 메세지 테이블을 만들어줬습니다. 

지난번에 소켓통신을 통해서 메세지를 가져왔고요.
이번엔 먼저 간단하게 룸이랑 룸 생성후 리스트만 불러오도록 했습니다.
 
room 

@Entity
@Table(name = "Room")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Room {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ROOM_SEQ")
    @SequenceGenerator(name = "ROOM_SEQ", sequenceName = "ROOM_SEQ", allocationSize = 1)
    @Column(name = "ID")
    private Long id;

    private String roomId;

    private String ip;

    @CreationTimestamp
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Message> messages = new ArrayList<>();

}

 
 jparepository는 간단하게 생략하고 리스트 불러오는 서비스쪽 코드는 

 @Override
    public Page<RoomListDTO> findAll(int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        Page<Room> rooms= roomRepository.findAll(pageable);

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        Page<RoomListDTO> roomDTOs=rooms.map(room ->RoomListDTO.builder()
                .id(room.getId())
                .roomId(room.getRoomId())
                .createdAt(room.getCreatedAt().format(formatter))
                .build());
        return roomDTOs;
    }

리스트들을 불러올거고요. 
dto는 

@Getter
@Setter
@Builder
public class RoomListDTO {
    private Long id;
    private String roomId;
    private String createdAt;
}

해당 파라미터들을 가져와서 룸리스트를 보여줄겁니다. 
프론트코드는 ajax를 통해서 가져올거고요.

function getRoomList(){
    var page = 0;
    var size = 10;

    // Ajax 호출
    $.ajax({
        url: '/room/' + page + '/' + size,
        type: 'GET',
        dataType: 'json',
        success: function(response) {
            var rooms = response.content;
            var tbody = $('#roomTable tbody');
            tbody.empty(); 

            if(rooms && rooms.length > 0) {
                $.each(rooms, function(index, room) {
                    var tr = $('<tr></tr>');
                    tr.append('<td>' + room.id + '</td>');
                    tr.append('<td>' + room.roomId + '</td>');
                    tr.append('<td>' + room.createdAt + '</td>');
                    tbody.append(tr);
                });
            } else {
                tbody.append('<tr><td colspan="3">No rooms available.</td></tr>');
            }
        },
        error: function(xhr, status, error) {
            console.error("Ajax 호출 에러: ", error);
        }
    });
}

 
html 내용입니다. 

<h1 style="text-align: center;">Room List </h1>
<table id="roomTable">
    <thead>
    <tr>
        <th>ID</th>
        <th>방 제목</th>
        <th>등록일</th>
    </tr>
    </thead>
    <tbody>
    <!-- 데이터가 여기 채워집니다 -->
    </tbody>
</table>

 
 

구조 :

 

STOMP

STOMP은 **"Simple (or Streaming) Text Oriented Messaging Protocol"**의 약자입니다. 웹소켓(WebSocket)을 사용할 때 메시지를 주고받기 위한 프로토콜 중 하나로, 텍스트 기반의 메시징 프로토콜입니다.
 
구조를 보시면 수신자는 topic or app을 통해서 request channel을 보냅니다. 
아래 소스 코드를 보시면 구성 요소에서  /topic, /app으로 설정되어 있는걸 알수 있습니다. 

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic"); // 메시지 브로커 경로
        config.setApplicationDestinationPrefixes("/app"); // 클라이언트 요청 경로
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/chat") // WebSocket 엔드포인트
                .addInterceptors(new IpHandshakeHandler())
                .setAllowedOriginPatterns("*")
                .withSockJS(); // SockJS 사용
    }
}

@EnableWebSocketMessageBroker

  • WebSocket 메시지 브로커를 활성화하는 어노테이션입니다.
  • STOMP 프로토콜을 통해 메시지 라우팅과 처리 기능을 제공합니다.

WebSocketMessageBrokerConfigurer

  • WebSocket과 관련된 설정을 커스터마이징할 수 있도록 제공되는 인터페이스입니다.
  • configureMessageBroker와 registerStompEndpoints 메서드를 구현하여 메시지 브로커와 엔드포인트를 설정합니다.

configureMessageBroker(MessageBrokerRegistry config)

메시지 브로커를 설정하는 메서드입니다.

  • config.enableSimpleBroker("/topic")
    • /topic 경로를 사용하는 간단한 메시지 브로커를 활성화합니다.
    • 이 브로커는 구독(Pub/Sub) 모델을 지원하며, 메시지를 특정 대상(destination)으로 전달합니다.
    • 예를 들어, 클라이언트가 /topic/chat을 구독하면 해당 경로로 발행된 메시지를 받을 수 있습니다.
  • config.setApplicationDestinationPrefixes("/app")
    • 클라이언트가 서버로 메시지를 보낼 때 사용할 경로의 접두사를 설정합니다.
    • 클라이언트가 /app/chat으로 메시지를 보내면, 해당 요청은 서버의 컨트롤러에서 처리됩니다.
    • 예를 들어, 서버 측 컨트롤러에서 @MessageMapping("/chat")을 사용해 이 경로를 처리할 수 있습니다.

/topic 을 통해서 채팅 방을 생성합니다. 
관련 roomid가 동일한 구독자들은 그 안에서 메세지를 볼수 이게 됩니다. 
 
메세지를 보내게 되면 /app/chat을 통해서 메세지를 보냅니다. 

백엔드에서 해당 채팅룸의 방이 어디서 보내줘야할지를 보내줍니다.

 

https://openssl-library.org/

 

OpenSSL Library

What and Who The OpenSSL software library is a robust, commercial-grade, full-featured toolkit for general-purpose cryptography and secure communication. It is developed under the OpenSSL Mission with support from the OpenSSL Foundation and OpenSSL Corpora

openssl-library.org

다운로드 

 

os: 리눅스  (우분트) 

open ssl 설치

sudo apt update
sudo apt install openssl libssl-dev

 

키 생성 후 암호 입력 

openssl genrsa -aes256 -out private_key.pem 2048

csr 생성 

openssl req -new -key private_key.pem -out request.csr

csr 정보 입력후  완료

Country Name (2 letter code) [AU]: KR
State or Province Name (full name) [Some-State]: Seoul
Locality Name (eg, city) []: Seoul
Organization Name (eg, company) [Internet Widgits Pty Ltd]: My Company Inc.
Organizational Unit Name (eg, section) []: IT Department
Common Name (e.g. server FQDN or YOUR name) []: www.example.com
Email Address []: admin@example.com

A challenge password []: 
An optional company name []:

 

Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "start_value" not found 

 

spring.jpa.hibernate.ddl-auto=update

를 update 에서 create로 

spring.jpa.hibernate.ddl-auto=create

 

대부분은 기존 테이블/시퀀스가 꼬인 경우여서 그런거여서 새롭게 다시 만들어주면 해결됩니다. 

Optional이란?
Optional은 Java 8부터 추가된 클래스로, ‘값이 있을 수도 있고 없을 수도 있다’는 개념을 명시적으로 표현하기 위해 사용됩니다. 전통적으로 Java에서 메서드가 ‘값이 없을’ 경우에는 null을 반환하거나 예외를 던져 왔습니다. 그러나 이러한 방식은 NullPointerException(NPE)의 위험이나 ‘이 메서드가 null을 반환할 수 있다’는 사실을 코드만으로는 명확히 알기 어렵다는 문제가 있었습니다.

Optional을 사용하면 다음과 같은 이점이 있습니다.

  1. 명시적 표현
    • ‘값이 없을 수도 있음’을 Optional이란 반환 타입만으로도 알 수 있어 코드 가독성과 안정성이 향상됩니다.
    • 메서드 반환 타입이 Optional<T>라면, 그 메서드 호출 결과가 null이 될 수도 있음을 암묵적으로 알 수 있게 됩니다.
  2. null 처리 로직 간소화
    • null 체크를 직접 하기보다는 ifPresent, orElse, orElseThrow 등의 메서드를 사용하여 간결하게 처리할 수 있습니다.
    • 이를 통해 NullPointerException 발생 가능성을 줄이고, 코드가 보다 함수형 스타일을 따르게 됩니다.
  3. 함수형 스타일 메서드 체이닝
    • map, flatMap, filter 같은 고차 함수들을 사용할 수 있어, 값의 존재 여부를 확인하는 반복적인 if (x != null) 구문을 대체할 수 있습니다.

언제 Optional을 사용해야 할까?

  1. ‘반환할 값이 있을 수도 없을 수도 있음’을 명확히 표현해야 하는 경우
    • 예) findById 메서드처럼 DB나 컬렉션에서 어떤 값을 찾는데, 그 값이 존재하지 않을 수도 있는 상황에서 결과를 담는 경우.
  2. API 설계 단계에서 메서드가 null이 될 가능성을 명시하고자 할 때
    • 메서드에서 null을 반환할 수 있다는 사실을 주석이 아닌 타입 자체로 전달해, 오용 가능성을 줄일 수 있습니다.
  3. 값의 부재를 예외 상황으로 보고 싶지 않은 경우
    • 예외를 던져야 할 수준은 아니지만, 그냥 null을 반환하기엔 직관적이지 않을 때 Optional을 사용합니다.

언제 사용하지 않아야 할까?

  1. 필드(멤버 변수)로 쓰지 말 것
    • Optional은 ‘값이 있을 수도, 없을 수도 있는 객체’를 표현하기 위한 것이지, 엔티티나 DTO의 필드를 선언할 때 쓰는 용도로 만들어진 것이 아닙니다.
    • 필드에 Optional을 두면 직렬화/역직렬화에서 문제가 생길 수 있고, 불필요한 레퍼 객체가 중첩되어 성능이 저하될 수 있습니다.
  2. 메서드 파라미터로 사용 지양
    • 파라미터에 Optional을 사용하기보다는 오버로딩, 기본값 설정, Builder 패턴 등을 사용하는 것이 더 권장됩니다.
  3. 컬렉션을 담는 Optional
    • “값이 없을 수도 있음”을 표현해야 하는 상황에서 컬렉션 자체가 이미 ‘비어 있음(empty) 또는 있음’을 표현할 수 있습니다. Optional<Collection<T>>를 사용하기보다는 빈 컬렉션을 반환하거나 null 이외의 다른 방법으로 처리하는 편이 좋습니다.
public class UserService {
    private final Map<Long, User> userStore = new HashMap<>();

    // Optional을 통한 반환
    public Optional<User> findUserById(Long id) {
        return Optional.ofNullable(userStore.get(id));
    }

    public void printUserName(Long id) {
        Optional<User> maybeUser = findUserById(id);

        // ifPresent: 값이 있으면 람다 실행
        maybeUser.ifPresent(user -> System.out.println(user.getName()));

        // orElse: 값이 없으면 대체값 사용
        User user = maybeUser.orElse(new User("익명"));
        System.out.println("유저명: " + user.getName());

        // orElseThrow: 값이 없으면 예외 던지기
        User userOrException = maybeUser.orElseThrow(() -> new IllegalArgumentException("유저를 찾을 수 없습니다."));
        System.out.println("필수 유저명: " + userOrException.getName());

        // map: 값이 있으면 변환
        String userName = maybeUser.map(User::getName).orElse("No Name");
        System.out.println("User Name: " + userName);
    }
}

'프로그래밍언어 > JAVA' 카테고리의 다른 글

Virtual Thread(가상 스레드)  (0) 2024.09.01
hashcode와 equals  (0) 2024.08.05
Hibernate ?  (0) 2024.03.31
순수 JAVA JPA  (0) 2024.03.17
getDeclaredConstructor().newInstance()?  (0) 2024.03.16

+ Recent posts