[Elasticsearch Config]

package com.example.coupang.search.config;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 🛠 ElasticsearchConfig
 * Elasticsearch 연결을 위한 Spring Boot 설정 클래스
 */
@Configuration // 📌 Spring에서 이 클래스를 설정(Configuration) 클래스로 인식하도록 함
public class ElasticsearchConfig {

    // 📌 application.properties 또는 application.yml에서 값을 가져옴
    @Value("${elasticsearch.host}")
    private String host; // Elasticsearch 호스트 (예: "localhost" 또는 "127.0.0.1")

    @Value("${elasticsearch.port}")
    private Integer port; // Elasticsearch 포트 (보통 9200)

    @Value("${elasticsearch.username}")
    private String username; // Elasticsearch 인증을 위한 사용자명

    @Value("${elasticsearch.password}")
    private String password; // Elasticsearch 인증을 위한 비밀번호

    @Value("${elasticsearch.fingerprint:}") // 기본값을 빈 문자열("")로 설정
    private String fingerprint; // (옵션) 보안 인증을 위한 SSL fingerprint

    private final ObjectMapper objectMapper;

    /**
     * 🔹 생성자 주입 방식으로 ObjectMapper를 받음
     * - `JavaTimeModule`을 등록하여 LocalDateTime 직렬화를 지원
     */
    public ElasticsearchConfig(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
        this.objectMapper.registerModule(new JavaTimeModule()); // ✅ Java 날짜/시간 타입(LocalDateTime) 지원
    }

    /**
     * 🛠 ElasticsearchClient Bean 생성
     * - Elasticsearch 서버와의 연결을 위한 Java API 클라이언트 객체 생성
     * - 보안 인증 및 JSON 직렬화를 설정
     */
    @Bean
    public ElasticsearchClient elasticsearchClient() {
        // 🔹 기본 인증을 위한 `CredentialsProvider` 객체 생성
        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));

        // 🔹 RestClient를 생성하여 Elasticsearch 서버와 HTTP 통신을 담당
        RestClient restClient = RestClient.builder(new HttpHost(host, port, "http")) // HTTP 프로토콜로 연결
                .setHttpClientConfigCallback(httpClientBuilder -> {
                    if (fingerprint != null && !fingerprint.isEmpty()) {
                        // 🔹 SSL 인증서 검증을 위한 컨텍스트 설정 (SSL fingerprint 기반 보안)
                        httpClientBuilder = httpClientBuilder.setSSLContext(
                                TransportUtils.sslContextFromCaFingerprint(fingerprint)
                        );
                    }
                    // 🔹 기본 인증 (Username/Password) 적용
                    return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
                })
                .build(); // 최종 RestClient 객체 생성

        // 🔹 JSON 직렬화를 위해 Jackson 기반의 JSON Mapper 생성
        JacksonJsonpMapper jacksonJsonpMapper = new JacksonJsonpMapper(objectMapper);

        // 🔹 ElasticsearchTransport 객체 생성
        ElasticsearchTransport transport = new RestClientTransport(restClient, jacksonJsonpMapper);

        // 🔹 ElasticsearchClient 객체 생성 및 반환
        return new ElasticsearchClient(transport);
    }
}

 

[Elasticsearch Service]

package com.example.coupang.search.service;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
// Elasticsearch Java API Client. Elasticsearch와 통신하는 핵심 클래스
import co.elastic.clients.elasticsearch._types.ElasticsearchException;
import co.elastic.clients.elasticsearch._types.query_dsl.DisMaxQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchPhraseQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.DeleteByQueryRequest;
import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.*;
import com.example.coupang.search.entity.SearchKeyword;
// 검색 키워드 정보를 담는 Elasticsearch 문서(Entity) 클래스
import com.example.coupang.search.repository.SearchKeywordRepository;
// Spring Data Elasticsearch 리포지토리. CRUD 기능 제공
import co.elastic.clients.elasticsearch._types.aggregations.TermsAggregationExecutionHint;
// Terms Aggregation 최적화(실행 힌트) 지정용 enum
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 🛠 SearchService
 * Elasticsearch를 활용한 검색/저장/조회 기능을 제공하는 서비스 클래스
 */
@Slf4j // (Lombok) 로깅 기능 추가 -> log.info(), log.error() 등 메서드 사용 가능
@RequiredArgsConstructor // (Lombok) final 필드만 포함된 생성자를 자동 생성
@Service // Spring Service 계층으로 스캔되어 빈(Bean) 등록
public class SearchService {

    // ▶ Spring Data Elasticsearch 리포지토리 주입
    private final SearchKeywordRepository searchKeywordRepository;

    // ▶ Elasticsearch Java API Client 주입
    private final ElasticsearchClient client;

    // ▶ Elasticsearch 인덱스명 지정
    private static final String INDEX_NAME = "search_keywords";

    // ---------------------------------------------------------------------
    // 1. 검색어 저장/업데이트 기능
    // ---------------------------------------------------------------------

    /**
     * 검색어를 저장하거나 업데이트합니다.
     * 이미 존재하면 검색 횟수를 증가시키고, 없으면 새로 생성합니다.
     *
     * @param searchIc   (임의) 검색 요청 고유 식별자
     * @param searchText 실제 검색 문자열
     * @return 저장된 SearchKeyword 객체
     */
    public SearchKeyword saveOrUpdateSearchKeyword(String searchIc, String searchText) {
        // 🔹 기존에 같은 검색어가 있는지 확인
        SearchKeyword keyword = searchKeywordRepository.findBySearchText(searchText);

        if (keyword != null) {
            // 🔹 이미 존재한다면 count(검색 횟수) +1 증가
            keyword.setCount(keyword.getCount() + 1);
        } else {
            // 🔹 존재하지 않으면 새로 생성 후 기본값 설정
            keyword = new SearchKeyword();
            keyword.setSearchIc(searchIc);
            keyword.setSearchText(searchText);
            keyword.setCount(1);
            keyword.setTimestamp(LocalDateTime.now());
        }

        // 🔹 생성 또는 업데이트된 검색어를 Elasticsearch 인덱스에 저장
        return searchKeywordRepository.save(keyword);
    }

    // ---------------------------------------------------------------------
    // 2. 인기 검색어 조회 기능 (Aggregation 이용)
    // ---------------------------------------------------------------------

    /**
     * Elasticsearch Aggregation을 사용하여 인기 검색어(상위 10개)를 조회합니다.
     * - "searchText.keyword" 필드를 대상으로 Terms Aggregation 수행
     */
    public List<String> getPopularKeywords() throws IOException {
        // 🔹 SearchRequest: Aggregation을 설정하여 popular_searches라는 이름으로 Terms Aggregation 수행
        SearchRequest searchRequest = new SearchRequest.Builder()
                .index("search_keywords") // 대상 인덱스
                .aggregations("popular_searches", a -> a
                        .terms(t -> t
                                .field("searchText.keyword")  // ✅ 검색어의 keyword 필드 사용
                                .size(1000)))                 // 상위 1000개까지 추출
                .build();

        // 🔹 Elasticsearch에 요청 및 응답 받기
        SearchResponse<Void> response = client.search(searchRequest, Void.class);

        // 🔹 Aggregation 결과에서 인기 검색어 목록 추출
        List<String> popularKeywords = new ArrayList<>();

        // 🔹 "popular_searches"라는 이름의 Aggregation 결과 존재 여부 확인
        if (response.aggregations() != null && response.aggregations().containsKey("popular_searches")) {
            // 🔹 popular_searches -> stringTerms -> buckets
            response.aggregations()
                    .get("popular_searches")
                    .sterms()                // StringTerms를 의미
                    .buckets().array()       // Bucket 리스트
                    .forEach(bucket -> popularKeywords.add(bucket.key().stringValue()));
        } else {
            // 🔹 Aggregation 결과가 없을 경우
            System.out.println("⚠️ Aggregation 결과가 없습니다!");
        }

        return popularKeywords;
    }

    /**
     * getPopularKeywords()를 최적화한 버전
     * - executionHint를 Map으로 설정하여 메모리 사용 대비 성능을 조절
     */
    public List<String> getPopularKeywordsOptimized() throws IOException {
        // 🔹 SearchRequest 생성 (executionHint = Map)
        SearchRequest searchRequest = new SearchRequest.Builder()
                .index("search_keywords")
                .aggregations("popular_searches", a -> a
                        .terms(t -> t
                                .field("searchText.keyword")      // keyword 필드
                                .size(1000)                       // 상위 1000개
                                .executionHint(TermsAggregationExecutionHint.Map))) // ✅ 실행 힌트 (Map) 적용
                .build();

        // 🔹 Elasticsearch에 요청 및 응답 받기
        SearchResponse<Void> response = client.search(searchRequest, Void.class);

        // 🔹 popular_searches Aggregation 결과 파싱
        List<String> popularKeywords = new ArrayList<>();
        response.aggregations()
                .get("popular_searches")
                .sterms()
                .buckets().array()
                .forEach(bucket -> popularKeywords.add(bucket.key().stringValue()));

        return popularKeywords;
    }

    /**
     * getPopularKeywords()를 더 빠르게 만든 버전
     * - requestCache(true)를 통해 반복되는 쿼리에 대한 캐시 활성화
     */
    public List<String> getPopularKeywordsFastest() throws IOException {
        // 🔹 SearchRequest 생성 (requestCache = true)
        SearchRequest searchRequest = new SearchRequest.Builder()
                .index("search_keywords")
                .aggregations("popular_searches", a -> a
                        .terms(t -> t
                                .field("searchText.keyword")
                                .size(1000)))
                .requestCache(true) // ✅ 쿼리 캐싱 활성화
                .build();

        // 🔹 Elasticsearch에 요청 및 응답 받기
        SearchResponse<Void> response = client.search(searchRequest, Void.class);

        // 🔹 popular_searches Aggregation 결과 파싱
        List<String> popularKeywords = new ArrayList<>();
        response.aggregations()
                .get("popular_searches")
                .sterms()
                .buckets().array()
                .forEach(bucket -> popularKeywords.add(bucket.key().stringValue()));

        return popularKeywords;
    }

    // ---------------------------------------------------------------------
    // 4. 도서 검색 기능 (고급 검색 쿼리, 하이라이팅 적용)
    // ---------------------------------------------------------------------

    /**
     * 검색어(keyword)를 이용해 Elasticsearch에서 검색을 수행
     * - DisMax 쿼리로 여러 Match 쿼리를 결합해 가장 높은 스코어를 채택
     * - 하이라이팅(Highlight) 적용
     *
     * @param keyword 사용자가 입력한 검색어
     * @return 검색 결과로 매핑된 SearchKeyword 엔티티 목록
     */
    public List<SearchKeyword> searchKeywords(String keyword) {
        // 🔹 고정값 정의
        final String INDEX = "search_keywords";  // 검색할 인덱스
        final String FIELD_NAME = "searchText";  // 기본 검색 필드
        final Integer SIZE = 10;                 // 검색 결과 최대 개수

        // 🔹 boost 값 설정
        final Float KEYWORD_BOOST_VALUE = 2f;    // 단일 키워드 부스트
        final Float PHRASE_BOOST_VALUE = 1.5f;   // 문구(phrase) 부스트
        final Float LANGUAGE_BOOST_VALUE = 1.2f; // 언어별 필드 부스트
        final Float DEFAULT_BOOST_VALUE = 1f;    // 기본 부스트
        final Float PARTIAL_BOOST_VALUE = 0.5f;  // 부분 매칭 부스트

        // 🔹 검색어에 한글이 포함되었는지 체크 -> 필드 접미사 결정
        String[] fieldSuffixes = containsKorean(keyword)
                ? new String[]{"", "_chosung", "_jamo"} // 한글이면 초성/자모 필드도 매칭
                : new String[]{"", "_engtokor"}         // 영문이면 영->한 변환 필드

        // 🔹 fieldSuffix + boostValue를 함께 관리할 Map
        Map<String, Float> boostValueByMultiFieldMap = Map.of(
                "", KEYWORD_BOOST_VALUE,      // 기본 필드 (ex: "searchText") -> boost 2
                ".edge", DEFAULT_BOOST_VALUE, // Edge n-gram 필드 -> boost 1
                ".partial", PARTIAL_BOOST_VALUE // partial 필드 -> boost 0.5
        );

        // 🔹 멀티 필드 MatchQuery 리스트 생성
        List<Query> queryList = createMatchQueryList(fieldSuffixes, boostValueByMultiFieldMap, keyword)
                .stream()
                .map(MatchQuery::_toQuery) // MatchQuery를 Query로 변환
                .collect(Collectors.toList());

        // 🔹 언어별 필드 (한글 or 영어) 쿼리 추가
        String languageField = FIELD_NAME + (containsKorean(keyword) ? ".kor" : ".en");
        // Match 쿼리 (boost = 1.2f)
        queryList.add(createMatchQuery(keyword, languageField, LANGUAGE_BOOST_VALUE)._toQuery());
        // MatchPhrase 쿼리 (boost = 1.5f)
        queryList.add(createMatchPhraseQuery(keyword, languageField, PHRASE_BOOST_VALUE)._toQuery());

        // 🔹 DisMax 쿼리: 여러 개의 쿼리 중 가장 높은 스코어를 채택
        DisMaxQuery disMaxQuery = new DisMaxQuery.Builder()
                .queries(queryList)
                .build();

        // 🔹 하이라이팅 설정: 검색어 부분을 <strong> 태그로 감싸서 반환
        Highlight highlight = createHighlightFieldMap(Arrays.asList(
                FIELD_NAME,
                FIELD_NAME + ".en",
                FIELD_NAME + ".kor",
                FIELD_NAME + ".edge",
                FIELD_NAME + ".partial"
        ));

        // 🔹 최종 검색 요청 구성
        SearchRequest searchRequest = new SearchRequest.Builder()
                .index(INDEX)
                .size(SIZE)
                .query(q -> q.disMax(disMaxQuery)) // DisMax 쿼리 적용
                .highlight(highlight)             // 하이라이팅 적용
                .build();

        // 🔹 Elasticsearch에 검색 요청
        SearchResponse<SearchKeyword> response;
        try {
            response = client.search(searchRequest, SearchKeyword.class);
        } catch (ElasticsearchException e) {
            // 🛑 Elasticsearch에서 발생한 예외 처리
            throw new RuntimeException("Elasticsearch 검색 실패", e);
        } catch (IOException e) {
            // 🛑 I/O 예외 처리
            throw new RuntimeException("Elasticsearch I/O 실패", e);
        }

        // 🔹 검색 결과 수 파악
        TotalHits total = response.hits().total();
        boolean isExactResult = total != null && total.relation() == TotalHitsRelation.Eq;
        log.info("There are {}{} results",
                isExactResult ? "" : "more than ",
                total != null ? total.value() : 0);

        // 🔹 검색 결과 목록(Hit) -> 실제 문서(Source) 추출
        List<Hit<SearchKeyword>> hits = response.hits().hits();
        List<SearchKeyword> result = hits.stream()
                .map(Hit::source)
                .collect(Collectors.toList());

        log.info("Search result: {}", result);
        return result;
    }

    // ---------------------------------------------------------------------
    // 4-1. 검색 헬퍼 메서드들
    // ---------------------------------------------------------------------

    /**
     * 문자열에 한글이 포함되어 있는지 확인
     * @param text 입력 문자열
     * @return true면 한글 존재
     */
    private boolean containsKorean(String text) {
        return text != null && text.matches(".*[ㄱ-ㅎㅏ-ㅣ가-힣]+.*");
    }

    /**
     * 멀티 필드에 대해 boost 값을 달리하여 MatchQuery 리스트를 생성
     * @param fieldSuffixes      필드 접미사 배열
     * @param boostValueByMultiFieldMap  (".edge" -> 1.0, ".partial" -> 0.5 등)
     * @param keyword            검색 문자열
     * @return  MatchQuery 리스트
     */
    private List<MatchQuery> createMatchQueryList(String[] fieldSuffixes,
                                                  Map<String, Float> boostValueByMultiFieldMap, String keyword) {
        // 🔹 fieldSuffixes와 boostValueByMultiFieldMap를 조합하여 MatchQuery 생성
        return Arrays.stream(fieldSuffixes)
                .flatMap(fieldSuffix -> boostValueByMultiFieldMap.entrySet().stream()
                        .map(entry ->
                                createMatchQuery(keyword, "searchText" + fieldSuffix + entry.getKey(), entry.getValue())
                        )
                )
                .collect(Collectors.toList());
    }

    /**
     * 일반 MatchQuery 빌더
     * @param keyword  검색어
     * @param fieldName 매칭할 필드명
     * @param boostValue 부스트 값
     * @return MatchQuery
     */
    private MatchQuery createMatchQuery(String keyword, String fieldName, Float boostValue) {
        // 🔹 Elasticsearch Java API에서 제공하는 MatchQuery 빌더
        return new MatchQuery.Builder()
                .query(keyword)
                .field(fieldName)
                .boost(boostValue)
                .build();
    }

    /**
     * MatchPhraseQuery 빌더 (문구 검색)
     * @param keyword     검색어
     * @param fieldName   필드명
     * @param boostValue  부스트 값
     * @return MatchPhraseQuery
     */
    private MatchPhraseQuery createMatchPhraseQuery(String keyword, String fieldName, Float boostValue) {
        // 🔹 Elasticsearch Java API에서 제공하는 MatchPhraseQuery 빌더
        return new MatchPhraseQuery.Builder()
                .query(keyword)
                .field(fieldName)
                .boost(boostValue)
                .build();
    }

    /**
     * 하이라이팅 적용할 필드들의 Map을 생성
     * @param fieldNames 하이라이팅 적용할 필드 목록
     * @return Highlight 객체
     */
    private Highlight createHighlightFieldMap(List<String> fieldNames) {
        // 🔹 각 필드에 대해 <strong>, </strong> 태그로 감싸도록 설정
        Map<String, HighlightField> highlightFieldMap = new HashMap<>();
        for (String fieldName : fieldNames) {
            highlightFieldMap.put(fieldName,
                    new HighlightField.Builder()
                            .preTags("<strong>")
                            .postTags("</strong>")
                            .build());
        }
        // 🔹 최종 Highlight 객체 생성
        return new Highlight.Builder().fields(highlightFieldMap).build();
    }

    /**
     * 전체 문서 삭제 (DeleteByQuery)
     * @throws IOException Elasticsearch I/O 예외
     */
    public void deleteAllDocuments() throws IOException {
        // 🔹 DeleteByQueryRequest: matchAll() 쿼리를 사용하여 전체 삭제
        DeleteByQueryRequest request = DeleteByQueryRequest.of(d -> d
                .index(INDEX_NAME)
                .query(q -> q.matchAll(m -> m))
        );

        // 🔹 DeleteByQuery 실행
        DeleteByQueryResponse response = client.deleteByQuery(request);

        // 🔹 삭제된 문서 수 확인
        log.info("전체 문서 삭제 완료: {}건 삭제됨", response.deleted());
    }
}

 

search_keywords 인덱스 매핑(JSON) 파일

{
  "settings": {
    "number_of_shards": 1,  // 🔹 데이터 분산을 위한 샤드 개수 (1개 지정)
    "number_of_replicas": 1, // 🔹 데이터 백업을 위한 복제본 개수 (1개 지정)

    "analysis": {  // 🔹 텍스트 분석기(Analyzer) 설정
      "analyzer": {
        "default": {  // 🔹 기본(standard) 분석기 사용
          "type": "standard"
        }
      }
    }
  },

  "mappings": {  // 🔹 문서 필드 매핑 설정
    "properties": {  // 🔹 각 필드의 유형 및 설정 정의

      "searchIc": {
        "type": "keyword"  // 🔹 검색어 ID(고유 식별자) - 검색/정렬용으로 사용
      },

      "searchText": {
        "type": "text",  // 🔹 검색어 필드 (전체 텍스트 검색 가능)
        "analyzer": "standard", // 🔹 기본 텍스트 분석기(standard) 사용
        "fields": {  // 🔹 multi-fields 설정 (추가적인 필드 변환 지원)
          "keyword": {  // 🔹 keyword 타입 추가 (정확한 검색, 집계 가능)
            "type": "keyword",
            "ignore_above": 256  // 🔹 256자 이상은 무시
          }
        }
      },

      "count": {
        "type": "integer"  // 🔹 검색 횟수를 저장하는 필드
      },

      "timestamp": {
        "type": "date",  // 🔹 검색어 저장 시각을 기록하는 필드
        "format": "uuuu-MM-dd'T'HH:mm:ss||uuuu-MM-dd'T'HH:mm:ss.SSS" 
        // 🔹 다양한 날짜 형식 지원 (ISO-8601 표준)
      },

      "suggest": {  
        "type": "completion",  // 🔹 자동완성 기능을 위한 Completion 필드
        "analyzer": "simple",  // 🔹 기본 분석기(simple): 공백 기준으로 나누고 소문자로 변환
        "preserve_separators": true,  // 🔹 단어 구분자(예: 띄어쓰기, '-' 등) 유지 여부
        "preserve_position_increments": true,  // 🔹 단어 위치 보존 (중간 단어도 검색 가능)
        "max_input_length": 50,  // 🔹 자동완성 대상 문자열의 최대 길이 (50자)
        
        "contexts": [  // 🔹 컨텍스트 필터링 지원 (예: 특정 카테고리만 추천)
          {
            "name": "category",  // 🔹 필터링할 컨텍스트 필드명
            "type": "category"  // 🔹 카테고리 필터 사용
          }
        ]
      }
    }
  }
}

 

🔹 Elasticsearch를 활용한 검색어 자동완성 및 인기 검색어 분석 프로젝트 발표


1️⃣ 프로젝트 개요

이번 프로젝트에서는 Elasticsearch를 활용한 검색어 자동완성 및 인기 검색어 분석 시스템을 구축했습니다.
사용자가 검색창에 입력하는 검색어를 실시간으로 추천하고, 검색 빈도를 분석하여 인기 검색어를 제공하는 기능을 구현했습니다.
이를 위해 Elasticsearch의 Completion Suggester, 역인덱싱(Reverse Indexing), 샤드(Shards)와 복제본(Replicas) 설정,
그리고 다양한 분석기(Analyzer)를 적용하여 검색 성능을 최적화했습니다.


2️⃣ 프로젝트 목표

✅ 1. 실시간 검색어 자동완성 기능

  • 사용자가 검색창에 입력할 때, 입력된 일부 문자만으로도 적절한 검색어 추천
  • 검색어 추천 시 카테고리별 필터링 지원 (예: "아이폰" 입력 시 "아이폰 14", "아이폰 Pro" 추천)
  • 검색어의 자동완성 속도를 높이기 위해 Completion Suggester 적용

✅ 2. 인기 검색어 분석 기능

  • 최근 사용자가 많이 검색한 상위 검색어 리스트 제공
  • Elasticsearch의 Aggregation(집계) 기능을 활용하여 검색 빈도 분석
  • 검색 빈도를 기반으로 검색 추천 결과에 가중치 부여 (검색 횟수가 많을수록 높은 우선순위)

✅ 3. 성능 최적화

  • 역인덱싱(Reverse Indexing) 방식으로 검색 속도 향상
  • 샤드(Shard)와 복제본(Replica)를 조정하여 확장성 및 장애 대응력 확보
  • Ngram(엔그램), Edge Ngram 분석기를 적용하여 유사 검색어 추천 최적화

3️⃣ 프로젝트 아키텍처

🔹 데이터 저장 및 검색 구조

Elasticsearch는 역인덱싱(Reverse Indexing) 구조를 활용하여 데이터를 저장하고 빠르게 검색합니다.
데이터 저장 시, 일반적인 관계형 데이터베이스(RDBMS)의 행(row) 기반 저장 방식이 아니라,
각 문서(document)에 대해 토큰화(tokenizing) 및 색인(indexing) 과정을 거쳐 검색 속도를 높입니다.

🔹 역인덱싱(Reverse Indexing)

  • 일반적인 데이터베이스에서는 특정 데이터를 검색하려면 **전체 데이터를 탐색(Sequential Scan)**해야 합니다.
  • 하지만 Elasticsearch는 역인덱싱(Reverse Indexing) 방식을 사용하여 검색 속도를 획기적으로 향상시킵니다.
  • 예를 들어, "아이폰 14 프로"라는 검색어가 저장될 때:
    {
      "아이폰": [1],
      "14": [1],
      "프로": [1]
    }
    
    • 각 단어를 키로 삼고, 해당 단어가 등장하는 문서 ID를 저장하는 방식으로 색인이 구축됨.
    • 검색 시 특정 단어가 포함된 문서를 즉시 조회할 수 있어 검색 속도가 매우 빠름.

4️⃣ 주요 기능 및 기술 적용

🔹 1. Elasticsearch 인덱스 매핑 및 샤드(Shard) 설정

{
  "settings": {
    "number_of_shards": 1,  
    "number_of_replicas": 1  
  },
  "mappings": {
    "properties": {
      "searchText": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "suggest": {
        "type": "completion",
        "analyzer": "simple",
        "contexts": [
          {
            "name": "category",
            "type": "category"
          }
        ]
      }
    }
  }
}

📌 적용된 기술

  • 샤드(Shards): number_of_shards: 1
    • 단일 노드에서 운영되므로 샤드 개수를 1로 설정.
    • 검색 성능 최적화를 위해 추후 클러스터 확장 가능하도록 설계.
  • 복제본(Replicas): number_of_replicas: 1
    • 가용성을 높이기 위해 복제본을 1개 추가 (장애 발생 시 자동 복구 가능).
  • Completion Suggester 적용: "suggest" 필드를 completion 타입으로 설정.
    • 자동완성 추천을 빠르게 제공하기 위해 적용.

🔹 2. 검색어 자동완성 기능 (Completion Suggester 활용)

사용자가 검색창에 입력하는 초성, 중간 단어 포함 검색이 가능하도록 설정.

{
  "suggest": {
    "text": "아이폰",
    "completion": {
      "field": "suggest",
      "contexts": {
        "category": ["아이폰 14"]
      }
    }
  }
}

📌 주요 특징

  • "아이폰"을 입력하면 "아이폰 14", "아이폰 14 Pro" 등을 자동 추천.
  • 카테고리(Contexts)를 활용하여 특정 범주 내에서 추천 가능.

🔹 3. 인기 검색어 분석 기능 (Aggregation 활용)

{
  "aggregations": {
    "popular_searches": {
      "terms": {
        "field": "searchText.keyword",
        "size": 10
      }
    }
  }
}

📌 주요 특징

  • terms aggregation을 사용하여 상위 검색어 10개 조회.
  • "searchText.keyword" 필드를 사용하여 정확한 단어 기준으로 검색.

🔹 4. 검색 성능 최적화를 위한 분석기(Analyzer) 적용

분석기 설명
standard 기본 영어/한글 토큰화 지원
simple 소문자로 변환 후 토큰화
ngram 입력된 단어를 작은 조각으로 나누어 검색 정확도 향상
edge_ngram 단어의 앞부분을 기반으로 검색 가능하게 설정 (자동완성 추천 최적화)

🔹 예제: edge_ngram 적용 시 검색 동작

{
  "settings": {
    "analysis": {
      "tokenizer": {
        "edge_ngram_tokenizer": {
          "type": "edge_ngram",
          "min_gram": 1,
          "max_gram": 10,
          "token_chars": ["letter", "digit"]
        }
      }
    }
  }
}
  • "아이폰"을 입력하면 "아이폰 14", "아이폰 14 프로", "아이폰 13" 등 연관 검색어 추천 가능.

5️⃣ 프로젝트 성과 및 결론

성공적인 성능 향상

  • 기존의 RDBMS 기반 검색 대비 평균 3배 이상 빠른 검색 성능을 확보.
  • 완전한 자동완성 기능을 제공하며, 검색 정확도를 향상.

Elasticsearch 기능 최적 활용

  • 샤드(Shard)와 복제본(Replica)를 최적화하여 장애 대응력 향상.
  • 역인덱싱 구조를 활용하여 실시간 검색 성능을 개선.

실제 서비스에 적용 가능

  • 다양한 전자상거래(eCommerce) 플랫폼 및 콘텐츠 검색 서비스에서 활용 가능.

 


📌 Elasticsearch vs 관계형 데이터베이스 (RDBMS) 비교 표

Elasticsearch 개념 RDBMS 개념 일상적인 예시

Elasticsearch 개념 RDBMS 개념 일상적인 예시
인덱스 (Index) 데이터베이스 (Database) 도서관
샤드 (Shard) 파티션 (Partition) 도서관에서 여러 서고(책장)로 책을 나누어 보관
타입 (Type) (현재 제거됨) 테이블 (Table) 한 개의 엑셀 파일에서 여러 개의 시트(예: "상품 목록", "주문 목록")
문서 (Document) 행 (Row) 엑셀의 한 줄(예: "사용자 정보 1개")
필드 (Field) 열 (Column) 엑셀의 열(예: "이름", "나이", "주소")
매핑 (Mapping) 스키마 (Schema) 엑셀에서 열의 데이터 유형을 미리 지정하는 것(예: "날짜", "숫자")
Query DSL SQL 자연어 검색 vs 데이터베이스 질의문(SQL)
역인덱싱 (Reverse Indexing) B-Tree 인덱스 책의 색인(Index) vs 사전에서 단어를 찾는 방식

📌 추가 설명

  • **Elasticsearch의 인덱스(Index)**는 **RDBMS의 데이터베이스(Database)**와 유사하며,
    여러 개의 문서(Document, RDBMS의 행(Row))를 저장합니다.
  • **샤드(Shard)는 RDBMS의 파티션(Partition)**과 비슷하게, 데이터를 여러 개로 나누어 저장하고 병렬 처리하여 성능을 향상시킵니다.
  • Query DSL은 SQL과 유사한 역할을 하지만, Elasticsearch는 NoSQL 기반이므로 JSON 형태로 쿼리를 작성합니다.
  • **역인덱싱(Reverse Indexing)**은 Elasticsearch에서 데이터를 저장할 때 색인(검색 속도를 빠르게 하기 위한 데이터 구조)을 생성하는 방식으로,
    일반적인 관계형 데이터베이스의 B-Tree 인덱스 방식과 차이가 있습니다.

✅ 정리

위 표를 통해 Elasticsearch와 관계형 데이터베이스(RDBMS)의 차이점을 쉽게 이해할 수 있습니다.
Elasticsearch는 **빠른 검색을 위해 최적화된 구조(역인덱싱, 샤드, 문서 기반 저장 방식 등)**를 사용하며,
RDBMS는 **정확한 관계형 데이터 관리(테이블, 스키마, SQL 질의문 등)**를 기반으로 운영됩니다. 🚀


이번 프로젝트에서는 검색어 자동완성과 인기 검색어 분석 기능을 구현했습니다.

Completion Suggester를 활용해 사용자가 검색어를 입력할 때 실시간으로 추천하는 기능을 개발했고,

Aggregation(집계) 기능을 적용해 많이 검색된 키워드를 분석하는 시스템을 구축했습니다.

더 나아가 100만건의 데이터를 첨부하여 필터를 적용한 인기 검색어 확인과 적용하지 않은 검색어 확인을 통해 속도의 변화를 관찰하였습니다.

또한, Elasticsearch의 인덱싱 과정에서 직렬화(Serialization) 이슈가 발생하여 이를 해결하는 데 어려움이 있었습니다.
이를 개선하기 위해 JSON 데이터 구조를 정리하고, 직렬화 방식을 최적화하여 정상적으로 데이터를 저장할 수 있도록 했습니다.

 

[1 page]

 

이번 프로젝트에서는 검색어 자동완성과 인기 검색어 분석 기능을 구현했습니다.
Completion Suggester를 활용해 사용자가 검색어를 입력할 때 실시간으로 추천하는 기능을 개발했고,

Aggregation(집계) 기능을 적용해 많이 검색된 키워드를 분석하는 시스템을 구축했습니다.

 

[2 page]

엘라스틱 서치에서도 성능 향상을 위하여, 힌트(맵), 카테고리화를 적용, 성능의 변화를 직접 확인해 볼 수 있었습니다.

위는 100만개의 데이터를 추가, 각 검색 방식의 수행시간을 비교한 표입니다.

 

차례대로 집계만을 사용한 방식, 맵으로 구현한 힌트를 적용한 방식, 카테고리를 적용한 방식순으로 속도가 점차 빨라졌습니다.

 

[3 page]

{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "default": {
          "type": "standard"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "searchIc": {
        "type": "keyword"
      },
      "searchText": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "count": {
        "type": "integer"
      },
      "timestamp": {
        "type": "date",
        "format": "uuuu-MM-dd'T'HH:mm:ss||uuuu-MM-dd'T'HH:mm:ss.SSS"
      },
      "suggest": {
        "type": "completion",
        "analyzer": "simple",
        "preserve_separators": true,
        "preserve_position_increments": true,
        "max_input_length": 50,
        "contexts": [
          {
            "name": "category",     
            "type": "category"      
          }
        ]
      }
    }
  }
}

[3 page]

또한, Elasticsearch의 인덱싱 과정에서 매핑 이슈가 발생하여 이를 해결하는 데 어려움이 있었습니다.
이를 개선하기 위해 JSON 데이터 구조를 정리하고, 직렬화 방식을 최적화하여 정상적으로 데이터를 저장할 수 있도록 했습니다.

'Project' 카테고리의 다른 글

숙련 CRUD 기반의 테스트, 개선 과제  (0) 2025.01.06

Level 1: 기본 기능 구현 및 코드 개선

1. 코드 개선 퀴즈 - @Transactional의 이해

  • 할 일을 저장하는 API(/todos) 호출 시 Connection is read-only 에러가 발생.
  • 정상적으로 할 일을 저장할 수 있도록 @Transactional 설정을 수정.

2. 코드 추가 퀴즈 - JWT의 이해

  • 요구사항:
    • User 테이블에 nickname 컬럼 추가 (중복 가능).
    • JWT에서 nickname을 꺼내 프론트엔드에 전달.

3. 코드 개선 퀴즈 - AOP의 이해

  • changeUserRole() 메서드 실행 전에 로그를 남기는 AOP 구현.
  • AOP 클래스 AdminAccessLoggingAspect 수정 필요.

4. 테스트 코드 퀴즈 - 컨트롤러 테스트의 이해

  • todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다() 테스트 실패.
  • 테스트가 정상적으로 통과하도록 수정.

5. 코드 개선 퀴즈 - JPA의 이해

  • 할 일 검색 기능 확장:
    • weather 조건으로 검색 (조건은 선택적).
    • 수정일 기준으로 기간 검색 (기간 조건도 선택적).
  • JPQL을 활용해 조건 처리.

Level 2: 고급 JPA 기능 구현

6. JPA Cascade

  • 할 일을 생성한 사용자가 자동으로 담당자로 등록되도록 cascade 설정.

7. N+1 문제 해결

  • CommentController의 getComments() API에서 N+1 문제 발생.
  • JPA fetch join을 사용하여 문제 해결.

8. QueryDSL

  • JPQL로 작성된 findByIdWithUser 메서드를 QueryDSL로 변경.
  • N+1 문제 없이 데이터 조회 구현.

9. Spring Security

  • 기존 Filter와 Argument Resolver 코드를 Spring Security로 변경.
  • 기존 권한 및 접근 제어 로직 유지.
  • JWT 기반 인증 방식 유지.

Level 3: 도전 과제

10. QueryDSL을 사용한 검색 기능 구현

  • 검색 조건:
    • 제목 키워드 검색 (부분 일치).
    • 생성일 범위 검색 (최신순 정렬).
    • 담당자 닉네임 검색 (부분 일치).
  • 검색 결과:
    • 일정의 제목만 반환.
    • 일정의 담당자 수와 댓글 개수 포함.
  • 기능 요구사항:
    • Projections를 사용해 필요한 데이터만 반환.
    • 페이징 처리된 결과 반환.

 

 

[회고]

 

이번 코드는 기능 개선과 오류의 확인, 간단한 구현이 전부였다. 오히려 내용적인 부분보다는 이론적인 부분을 저오학히 알고 있는지를 확인하는 느낌이 강했던 것 같다. 다만 이번 과제를 하면서 느낀점이, 제대로 하고 있는가?라는 질문이 나왔던 것 같다.

 

현재까지는 이론을 듣고 그저 필요한 기능에 맞춰 구현했지만, 개발자의 필요한 능력은 팀플을 하면서 느낀바로는 문제 해결능력이었다. 더 효율적인 코드를 위해 앞으로의 유연한 확장을 위해 개발을 하는 것이 가장 큰 목표라고 느꼈으며, 코드를 짜는 것 자체는 상당히 중요한 건 맞지만 조금 보조적인 느낌이라고 느꼈다.

 

이렇게 생각하고 나니, 지금 제대로 학습을 하고 있는가?라는 생각이 들 수 밖에 없었다.
1. 왜 이걸 썼는가?

= 레디스는 빠르니까 데이터를 매핑해서 사용하거나 요청하는데 있어 조금이라도 더 속도가 빨라질 것 같아서 사용했다.
2. 레디스는 안정성이 떨어지는데 그 점은 괜찮은가?

= 안정성이 떨어지는 부분을 인지하고 있고 일시적으로만 저장될 데이터만 저장에 이용하였고 처리가 완전히 끝난 완전한 데이터만 Mysql로 저장했다.

 

지금까지는 이렇게 생각했다. 하지만 이건 내가 그나마 많이 사용한 기능이기에 부족하지만 이정도라도 답변이 가능했지만, 사실 생각지 못했던 marven gradle 중 왜 gradle을 사용했는가와 같은 답변에는 대답하기 힘들다는 생각이 들었다. 무엇보다 시간이 많이 없는 상태에서 어떻게 공부하는게 효과적인가 라는 질문이 계속 들었다.

 

이렇게 보니 앞으로의 최종 프로젝트에서 원래는 쿠버네티스, 엘라스틱서치와 같은 기능을 사용하려했지만, 과연 내가 이 기능의 이론이 아니라 정확히 이해하고 언제 사용해야하는지 그리고 각 단점을 어떻게 보완할지와 같은 질문에 대답할 수 있는가? 라는 질문도 생겼다.

 

"이론을 이해하고 적제 적소에 사용할 수 있는 능력, 그리고 각 기능의 단점을 보완하고 장점을 살려 문제를 해결하는 것"이게 현재까지 느끼는 개발자의 능력이었지만, 그저 코드만 짤 뿐이라면 위의 2개의 질문에 답할 수 없다.

 

과연 많은 기능을 포함한 프로젝트를 지금의 이해도로 만들어도 될까?

 

이론을 공부만 해서는 특정 상황을 질문을 하고 해결하라고 하면 답하기 힘들지 않을까?

 

그렇다면 어떻게 공부해야하는가, 아는 친구는 ORM의 작동 방식에 대해서도 상세히 알아야한다고 했다. 사실 자동으로 처리하니 그전까지는 ORM이 어노테이션을 이용해 자동으로 처리하는 것에 대해 아무생각이 없었다. 솔직히 여기까지 공부해야한다고? 생각이 들었다. 어떻게, 어디까지 공부해야하는걸까?

 

이 과제를 진행하면서 3가지가 가장 큰 고민이라는 생각이 든다.

 

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

[Spring] IOC & DI  (0) 2025.01.23
스프링 NEWSFEED 협업 프로젝트  (1) 2024.12.27
일정표를 만들어 보자! 업데이트!  (2) 2024.12.19
일정표를 만들어 보자!  (2) 2024.12.09
[ Spring ] 쇼핑몰 프로젝트 회고  (0) 2024.11.25

IOC와 DI 유의사항

 

1. IOC와 DI의 관계

  • **IOC(제어의 역전)**는 더 큰 개념입니다.
    • 객체의 제어권(생성과 실행 흐름 관리)을 개발자가 아닌 외부(프레임워크, 컨테이너)에 넘기는 설계 원칙입니다.
    • IOC는 객체 지향 설계에서 제어 흐름을 외부로 위임한다는 철학적 개념에 가깝습니다.
  • **DI(의존성 주입)**는 IOC를 구현하는 방식 중 하나입니다.
    • 의존성 주입은 IOC를 실현하는 구체적인 기술적 방법입니다.
    • 객체가 스스로 의존성을 생성하지 않고, 외부에서 필요한 의존성을 주입받는 것을 의미합니다.

즉, DI는 IOC를 구현하기 위한 방법 중 하나이며, 모든 DI는 IOC의 일종이지만, 모든 IOC가 DI는 아닐 수 있습니다.


2. IOC와 DI의 차이

항목 IOC(제어의 역전) DI(의존성 주입)
정의 프로그램의 제어 흐름(객체 생성과 실행)을 개발자가 아닌 외부로 위임하는 설계 원칙. 외부에서 객체의 의존성을 주입하여 결합도를 줄이고 유연성을 높이는 설계 기법.
포커스 제어권(흐름)의 역전: 누가 객체를 생성하고, 실행을 제어할지에 대한 개념적 설계. 의존성(Dependency)의 전달: 객체 간의 관계를 어떻게 설정할지에 대한 구체적인 기술.
범위 설계 철학(개념): 제어권을 외부로 넘기는 큰 틀의 원칙. 구현 방법(기술): 의존성을 외부에서 주입하는 특정한 방식.
실현 방식 DI, 이벤트 기반 시스템, 전략 패턴 등 다양한 방법으로 구현 가능. 주로 DI 컨테이너(Spring, Guice 등)를 사용하거나 수동으로 구현.
목적 결합도를 낮추고 제어 흐름을 외부에서 관리하도록 설계. 구체적인 의존성 전달을 통해 객체 간 관계를 설정.

3. 예시로 이해하기: IOC와 DI

IOC 예시 (제어의 역전)

전통적인 방식 (제어권 개발자 소유):

class EmailService {
    public void sendEmail(String message) {
        System.out.println("Sending Email: " + message);
    }
}

class NotificationService {
    private EmailService emailService;

    public NotificationService() {
        // 객체 생성과 의존성을 직접 관리
        this.emailService = new EmailService();
    }

    public void send(String message) {
        emailService.sendEmail(message);
    }
}
  • 문제점:
    • NotificationService가 EmailService의 구체적인 구현체에 강하게 결합.
    • 제어권(객체 생성 및 실행 흐름)이 개발자 코드 내부에 있음.

IOC 적용 (제어권 역전)

class EmailService {
    public void sendEmail(String message) {
        System.out.println("Sending Email: " + message);
    }
}

class NotificationService {
    private EmailService emailService;

    // 외부에서 객체를 주입받음 (제어권 역전)
    public NotificationService(EmailService emailService) {
        this.emailService = emailService;
    }

    public void send(String message) {
        emailService.sendEmail(message);
    }
}

class App {
    public static void main(String[] args) {
        // 객체 생성 및 의존성 관리가 외부(App 클래스)로 이동
        EmailService emailService = new EmailService();
        NotificationService notificationService = new NotificationService(emailService);

        notificationService.send("Hello World!");
    }
}
  • 제어권 역전:
    • 객체 생성 및 의존성 설정의 제어권이 App으로 이동.
    • NotificationService는 자신이 사용할 객체를 몰라도 동작.

DI 예시 (의존성 주입)

DI는 위의 IOC를 구현한 구체적인 방식입니다.

// 인터페이스를 통한 DI 적용
interface MessageService {
    void sendMessage(String message);
}

class EmailService implements MessageService {
    public void sendMessage(String message) {
        System.out.println("Sending Email: " + message);
    }
}

class SmsService implements MessageService {
    public void sendMessage(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

class NotificationService {
    private final MessageService messageService;

    // 생성자를 통한 의존성 주입
    public NotificationService(MessageService messageService) {
        this.messageService = messageService;
    }

    public void notify(String message) {
        messageService.sendMessage(message);
    }
}

public class Main {
    public static void main(String[] args) {
        // EmailService 주입
        MessageService emailService = new EmailService();
        NotificationService emailNotification = new NotificationService(emailService);
        emailNotification.notify("Hello via Email!");

        // SmsService 주입
        MessageService smsService = new SmsService();
        NotificationService smsNotification = new NotificationService(smsService);
        smsNotification.notify("Hello via SMS!");
    }
}
  • DI 특징:
    • NotificationService는 MessageService 인터페이스에 의존하므로 구체적인 구현체를 알 필요가 없습니다.
    • MessageService의 구현체는 외부에서 주입되므로, 의존성을 변경(예: SmsService → PushService)하기 쉽습니다.

4. IOC와 DI의 일상적인 비유

IOC(제어의 역전):

  1. 택배 서비스:
    • 전통적인 방식: 고객이 직접 가게에서 물건을 사고 가져옵니다. (고객이 제어권을 가짐)
    • IOC 방식: 택배 회사가 물건을 배송하고, 고객은 물건만 받습니다. (제어권이 택배 회사로 역전)
  2. 자동차 서비스:
    • 전통적인 방식: 운전자가 직접 자동차 수리를 관리.
    • IOC 방식: 정비소에 맡기면 정비사가 알아서 자동차를 고침.

DI(의존성 주입):

  1. 셰프와 재료:
    • 전통적인 방식: 셰프가 요리 재료를 직접 구입.
    • DI 방식: 주방 보조가 재료를 준비하고, 셰프는 받은 재료로 요리만 합니다.
  2. 콘센트와 전기:
    • 전통적인 방식: 전자제품이 직접 전기를 생성.
    • DI 방식: 콘센트(외부)가 전기를 공급하고, 전자제품은 연결만 합니다.

5. 요약

항목 IOC(제어의 역전) DI(의존성 주입)
개념 객체 생성과 제어 흐름을 외부로 위임하는 설계 원칙. 외부에서 객체의 의존성을 주입하여 결합도를 줄이는 기법.
관계 큰 철학적 개념(원칙). IOC를 구현하기 위한 구체적인 방법.
목적 객체 간 결합도를 낮추고 제어 흐름을 외부에서 관리. 객체의 의존성을 외부에서 설정해 다형성과 테스트 용이성 확보.
일상 비유 택배 회사가 고객 대신 물건을 배송. 셰프가 주방 보조로부터 재료를 주입받아 요리.

결론: DI는 IOC를 실현하기 위한 방법입니다. IOC는 큰 설계 원칙이고, DI는 그 원칙을 구현하는 하나의 도구입니다. 두 개념은 밀접히 연결되어 있지만, DI는 의존성을 주입하는 구체적인 기술이고, IOC는 객체 제어 흐름을 외부로 넘기는 더 큰 철학적 개념입니다.

 

 

IOC (Inversion of Control) 제어의 역전

 

IOC란?

**IOC(제어의 역전)**은 객체 생성 및 의존성 관리를 개발자가 아닌 외부에서 제어하는 설계 원칙입니다.

  • 기존에는 프로그램이 직접 객체를 생성하고 메서드를 호출하여 제어 흐름을 관리했지만, IOC에서는 이 역할을 외부(조립자, 설정자 또는 프레임워크)가 담당합니다.
  • 이는 객체 간의 결합도를 줄이고, 유연성과 확장성을 높이는 데 도움을 줍니다.
  • 원래는 각 클래스에서 필요한 객체를 생성해서 사용했지만, IOC는 이 객체나 의존성을 담당하는 클래스를 따로 만들고 이 클래스에 다른 클래스가 접근해서 해당 기능을 사용하는 방식이다. 결국 다양성, 결합도 감소를 위해 객체, 의존성을 담당하는 클래스를 따로 만들었다는 말

IOC의 핵심 개념

  1. 객체 생성과 의존성 주입의 외부 위임:
    • 프로그램이 직접 객체를 생성하지 않고, 필요한 객체를 외부에서 받아 사용합니다.
    • 예: 스프링 프레임워크는 컨테이너가 객체를 생성하고 필요한 의존성을 주입합니다.
  2. 제어 흐름의 역전:
    • 기존에는 개발자가 프로그램의 제어 흐름을 관리했지만, IOC에서는 프레임워크나 외부 설정자가 제어 흐름을 관리합니다.
    • 객체는 단순히 요청을 처리하는 역할만 수행하고, 어떤 객체가 주입될지는 알지 못합니다.
  3. 결합도를 낮추고 확장성을 높임:
    • 구체적인 구현체가 아닌 추상화된 인터페이스에 의존하도록 설계하여 다형성을 극대화합니다.

일상적인 예시로 이해하기

  1. 식당 운영 시스템:
    • 전통적인 방식: 셰프가 요리 재료를 직접 고르고, 요리를 만들고, 손님에게 서빙까지 직접 관리.
      • 셰프는 재료에 강하게 의존하므로, 재료가 바뀌면 모든 요리 방식을 수정해야 함.
    • IOC 방식: 식당 주인이 셰프에게 필요한 재료(의존성)를 전달하고, 셰프는 재료를 이용해 요리를 만듦.
      • 셰프는 재료를 직접 고르지 않으며, 어떤 재료가 주어질지 신경 쓰지 않음.
  2. 택시 호출 서비스:
    • 전통적인 방식: 승객이 직접 택시를 잡고, 특정 기사와 거래를 설정.
      • 특정 택시에 의존하므로 변경이 어려움.
    • IOC 방식: 승객은 호출 앱(플랫폼)을 사용해 택시를 요청하고, 플랫폼이 적합한 택시를 배정.
      • 승객은 배정된 기사에 대해 미리 알 필요가 없고, 플랫폼이 적절한 택시를 알아서 선택.
  3. 전자제품과 전원:
    • 전통적인 방식: 전자제품(객체)이 직접 전원을 생성.
      • 전자제품마다 전력 공급 방식이 달라지면 제품 내부를 수정해야 함.
    • IOC 방식: 전자제품은 콘센트에서 제공되는 전원을 이용.
      • 전자제품은 전원의 구체적인 공급 방식을 몰라도 작동 가능.

 

 

그래서 흐름 제어? 는 뭔 말일까?

 

**"실행을 제어한다"**는 말은 프로그램에서 어떤 객체가 메서드를 호출하고, 로직을 진행하며, 전체 흐름을 주도할지를 결정하는 주체가 누구인지를 의미합니다. 전통적인 방식에서는 개발자가 직접 객체를 생성하고 필요한 메서드를 호출하며 실행 흐름을 주도했습니다. 반면, **IOC(제어의 역전)**에서는 이 제어 흐름이 외부 프레임워크나 컨테이너로 넘어갑니다. = 기존에는 프로그래머가 클래스에서 어떤 클래스를 불러와서 실행하고 ~~~ 와 같은 실행의 흐름을 직접 구성해주었다면, 스프링에서는 이걸 스프링 프레임워크가 대신한다. 흔히 사용하는 어노테이션과 생성자의 객체 매개변수를 통해 DI를 수행한다.

 

+ 생성자의 객체 매개변수 의 경우 클래스 내부에서 객체를 정의하는 것이 아닌 외부 클래스에서 객체를 만들고 그걸 객체 매개변수 로 인식하기만 하니 이것또한 IOC에 해당한다.

 

+ "의존성을 주입받는다"는 말은, 클래스가 직접 필요한 객체를 생성하지 않고, 외부에서 만들어진 객체를 전달받아 사용한다 = 외부 객체에 의존하겠다. 이말이다.


1. 실행 흐름의 제어란?

전통적인 방식 (제어 흐름 개발자 소유):

  • 개발자가 직접 모든 객체를 생성하고, 그 객체의 메서드를 호출하여 실행을 제어합니다.
  • 실행의 주체는 개발자 코드 내부에 있습니다.

IOC 방식 (제어 흐름 외부 위임):

  • 객체의 생성 및 실행 흐름(메서드 호출의 순서와 방식)을 외부 컨테이너(예: 스프링)에 맡깁니다.
  • 개발자는 객체의 실행 방법이나 관계를 정의하지 않고, 외부에서 주입된 객체에만 집중합니다.
  • 실행의 주체가 외부로 넘어가므로 "제어의 역전"이 이루어진 것입니다.

2. 코드로 이해하기

(1) 전통적인 방식: 실행 흐름을 개발자가 직접 관리

class EmailService {
    public void sendEmail(String message) {
        System.out.println("Sending Email: " + message);
    }
}

class NotificationService {
    private EmailService emailService;

    public NotificationService() {
        // 객체 생성 및 관계 설정
        this.emailService = new EmailService();
    }

    public void notify(String message) {
        // 실행 흐름을 직접 제어
        emailService.sendEmail(message);
    }
}

public class Main {
    public static void main(String[] args) {
        NotificationService notificationService = new NotificationService();
        notificationService.notify("Hello, World!");
    }
}

실행 흐름 설명:

  1. Main 메서드에서 개발자가 NotificationService 객체를 생성.
  2. NotificationService는 내부적으로 EmailService 객체를 생성.
  3. notify 메서드를 호출하며, 실행 흐름을 개발자가 직접 제어.

문제점:

  • 모든 객체의 생성, 관계 설정, 실행 흐름을 개발자가 직접 관리하므로 코드가 복잡해짐.
  • 다른 의존성(SmsService)으로 변경하려면, 개발자가 기존 코드를 수정해야 함.

(2) IOC 방식: 실행 흐름을 외부 컨테이너에 위임

interface MessageService {
    void sendMessage(String message);
}

class EmailService implements MessageService {
    public void sendMessage(String message) {
        System.out.println("Sending Email: " + message);
    }
}

class SmsService implements MessageService {
    public void sendMessage(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

class NotificationService {
    private MessageService messageService;

    // 의존성을 외부에서 주입받음
    public NotificationService(MessageService messageService) {
        this.messageService = messageService;
    }

    public void notify(String message) {
        // 외부에서 주입된 객체를 사용해 실행 흐름을 진행
        messageService.sendMessage(message);
    }
}

class AppConfig {
    // 외부 컨테이너 역할: 객체 생성 및 관계 설정
    public static NotificationService createNotificationService(String type) {
        MessageService messageService;
        if (type.equalsIgnoreCase("email")) {
            messageService = new EmailService();
        } else if (type.equalsIgnoreCase("sms")) {
            messageService = new SmsService();
        } else {
            throw new IllegalArgumentException("Unknown type");
        }
        return new NotificationService(messageService);
    }
}

public class Main {
    public static void main(String[] args) {
        // 실행 흐름을 AppConfig(외부 컨테이너)가 관리
        NotificationService notificationService = AppConfig.createNotificationService("email");
        notificationService.notify("Hello via Email!");

        NotificationService smsNotification = AppConfig.createNotificationService("sms");
        smsNotification.notify("Hello via SMS!");
    }
}

실행 흐름 설명:

  1. AppConfig가 객체를 생성하고 의존성을 주입하여 실행 흐름을 관리.
  2. NotificationService는 어떤 구체적 객체가 사용되는지 알 필요 없이, 주입된 객체만 사용.
  3. Main 메서드는 AppConfig를 호출하여 필요한 객체를 가져오고 실행.

3. "실행 흐름을 외부로 넘긴다"의 본질

IOC에서 실행 흐름이 외부로 넘어간다는 것은 다음과 같은 점을 포함합니다:

  1. 객체 생성:
    • 개발자가 객체를 직접 생성하지 않고, 외부 컨테이너가 생성.
    • 개발자는 컨테이너가 제공한 객체를 사용만 함.
  2. 의존성 관리:
    • 개발자가 객체 간의 관계를 직접 설정하지 않음.
    • 외부 컨테이너가 필요한 객체를 주입하여 의존성을 관리.
  3. 실행 순서 제어:
    • 실행되는 메서드나 로직의 흐름을 외부에서 관리.
    • 개발자는 "어떤 객체가 주입될지", "어떻게 동작할지"를 신경 쓰지 않고, 주어진 역할에만 집중.

4. 일상적인 비유로 이해

전통적인 방식 (개발자가 직접 제어):

  • 상황: 개인이 직접 집을 짓는 경우
    1. 필요한 재료(의존성)를 직접 구매.
    2. 벽돌을 쌓고, 전기 배선을 연결하며, 도배까지 직접 실행.
    3. 모든 과정(흐름)을 스스로 제어.

IOC 방식 (실행 흐름 외부 위임):

  • 상황: 건축회사를 고용하여 집을 짓는 경우
    1. 건축회사(컨테이너)가 필요한 재료를 관리.
    2. 전기공, 배관공, 목수를 투입(의존성 주입)하여 작업.
    3. 집주인은 "이런 집을 원한다"는 요구만 하고, 실행 흐름은 건축회사가 관리.

5. 결론: 실행 흐름의 역전이란?

  • 전통적인 방식에서는 개발자가 객체 생성, 관계 설정, 메서드 호출 등 모든 실행 흐름을 직접 관리.
  • IOC에서는 실행 흐름(객체 생성, 의존성 주입, 메서드 호출 등)을 외부 컨테이너(예: 스프링 프레임워크)에 위임.
  • 개발자는 객체 간의 관계나 실행 흐름을 신경 쓰지 않고, 주어진 역할에만 집중할 수 있습니다.

즉, "실행을 제어한다"는 것은 객체 생성, 의존성 설정, 로직 실행 흐름을 포함하며, IOC에서는 이를 외부에서 관리하여 더 유연하고 확장 가능한 구조를 제공합니다.


IOC의 장점

  1. 유연성:
    • 새로운 기능 추가 시 기존 코드 수정이 필요 없음(OCP 원칙 준수).
  2. 결합도 감소:
    • 객체 간의 관계를 인터페이스로 추상화하여 변경에 강함.
  3. 테스트 용이성:
    • Mock 객체를 쉽게 주입하여 테스트 가능.
  4. 코드 재사용성:
    • 객체를 재사용할 수 있어 개발 속도가 빨라짐.

IOC 요약 표

항목 내용
정의 객체의 생성 및 의존성 관리를 개발자가 아닌 외부에서 제어하는 설계 원칙.
핵심 개념 1. 객체 생성 및 관리 외부 위임2. 제어 흐름 역전3. 결합도 감소와 다형성 극대화
장점 유연성, 유지보수 용이, 결합도 감소, 테스트 용이성, 코드 재사용성 증가
일상적인 예시 1. 식당에서 주인이 재료를 제공2. 택시 호출 앱3. 전자제품과 콘센트
대표적인 활용 프레임워크 스프링 프레임워크 (DI 컨테이너를 활용해 IOC 구현)

결론

IOC는 객체 지향 설계에서 **의존성 주입(DI)**과 함께 사용되어 프로그램의 유연성과 확장성을 극대화합니다. 이는 객체 간의 강한 결합을 제거하고, 코드 변경 없이 새로운 기능을 추가하거나 기존 기능을 교체할 수 있게 합니다.
일상 속 예시와 코드를 함께 보면, IOC는 **"중간에서 객체 관계를 설정하고 관리하는 조립자 역할을 외부에 위임"**하는 개념임을 쉽게 이해할 수 있습니다.

 

 

DI (Dependency Injection) 의존성 주입

 

1. DI란?

**DI(의존성 주입)**는 객체 간의 의존성을 외부에서 주입하여 객체 간의 결합도를 줄이고 유연성을 높이는 설계 원칙입니다.

  • 의존성(Dependency): 한 객체가 다른 객체를 사용할 때 그 객체를 의존한다고 합니다.
  • 의존성 주입(Injection): 의존하는 객체를 코드 내부에서 직접 생성하지 않고, 외부에서 주입받는 방식입니다.

2. DI의 핵심 개념

  1. 의존성 분리:
    • 객체는 필요한 의존성을 스스로 생성하지 않습니다.
    • 대신 외부에서 의존성을 주입받아 사용합니다.
  2. 유연성과 재사용성:
    • 객체가 구체적인 구현체가 아닌 인터페이스에 의존하므로, 의존성을 쉽게 교체할 수 있습니다.
    • 테스트 시 Mock 객체를 주입하거나, 새로운 구현체를 추가하는 것이 용이합니다.
  3. DI의 유형:
    • 생성자 주입(Constructor Injection): 의존성을 생성자를 통해 주입.
    • 세터 주입(Setter Injection): 세터 메서드를 통해 의존성을 주입.
    • 필드 주입(Field Injection): 필드에 직접 주입(주로 프레임워크에서 사용).

3. DI의 일상적인 예시

  1. 가전제품과 콘센트:
    • 의존성: 가전제품(객체)은 전기(의존성)가 필요합니다.
    • 전통적인 방식: 가전제품이 자체적으로 전기를 생성해야 합니다.
    • DI 방식: 가전제품은 콘센트(외부 주입)에서 전기를 받아 사용합니다. 가전제품은 전기의 세부 공급 방식(예: 태양광, 수력)을 몰라도 동작합니다.
  2. 택배 서비스:
    • 의존성: 고객은 물건을 받으려면 택배 서비스(의존성)가 필요합니다.
    • 전통적인 방식: 고객이 직접 물건을 구매하고 배달까지 처리.
    • DI 방식: 택배회사가 물건을 고객에게 전달. 고객은 물건이 어디서 왔는지 알 필요 없이, 사용만 하면 됩니다.
  3. 식당의 셰프와 재료:
    • 의존성: 셰프는 요리를 하려면 재료가 필요합니다.
    • 전통적인 방식: 셰프가 직접 재료를 구하고 요리를 합니다.
    • DI 방식: 주방 보조(외부 주입)가 재료를 제공하고, 셰프는 재료를 받아 요리만 합니다. 셰프는 재료가 어디서 왔는지 몰라도 요리를 할 수 있습니다.

4. 코드로 이해하기

1) 전통적인 방식

class EmailService {
    public void sendEmail(String message) {
        System.out.println("Sending email: " + message);
    }
}

class NotificationService {
    private EmailService emailService;

    public NotificationService() {
        // 직접 의존성 생성
        this.emailService = new EmailService();
    }

    public void notify(String message) {
        emailService.sendEmail(message);
    }
}

public class Main {
    public static void main(String[] args) {
        NotificationService notificationService = new NotificationService();
        notificationService.notify("Hello World!");
    }
}

문제점:

  1. NotificationService가 EmailService라는 구체적인 구현체에 강하게 결합되어 있습니다.
  2. 새로운 알림 방식(예: SmsService)을 추가하거나 테스트 시 Mock 객체로 대체하기 어려움.

2) DI 방식: 생성자 주입

// 의존성을 추상화
interface MessageService {
    void sendMessage(String message);
}

// 다양한 구현체
class EmailService implements MessageService {
    public void sendMessage(String message) {
        System.out.println("Sending email: " + message);
    }
}

class SmsService implements MessageService {
    public void sendMessage(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

class NotificationService {
    private final MessageService messageService;

    // 생성자를 통해 의존성을 주입받음
    public NotificationService(MessageService messageService) {
        this.messageService = messageService;
    }

    public void notify(String message) {
        messageService.sendMessage(message);
    }
}

public class Main {
    public static void main(String[] args) {
        // 의존성을 외부에서 주입
        MessageService emailService = new EmailService();
        NotificationService notificationService = new NotificationService(emailService);
        notificationService.notify("Hello via Email!");

        // 의존성을 다른 구현체로 교체
        MessageService smsService = new SmsService();
        NotificationService smsNotificationService = new NotificationService(smsService);
        smsNotificationService.notify("Hello via SMS!");
    }
}

특징:

  • NotificationService는 MessageService 인터페이스에만 의존합니다.
  • 의존성(EmailService나 SmsService)은 외부에서 주입되므로, NotificationService는 구현체가 무엇인지 알 필요가 없습니다.

3) DI 방식: 스프링 프레임워크를 활용

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

// 의존성을 추상화
interface MessageService {
    void sendMessage(String message);
}

@Component
class EmailService implements MessageService {
    public void sendMessage(String message) {
        System.out.println("Sending email: " + message);
    }
}

@Component
class NotificationService {
    private final MessageService messageService;

    @Autowired
    public NotificationService(MessageService messageService) {
        this.messageService = messageService;
    }

    public void notify(String message) {
        messageService.sendMessage(message);
    }
}

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        var context = SpringApplication.run(Main.class, args);

        NotificationService notificationService = context.getBean(NotificationService.class);
        notificationService.notify("Hello via Email!");
    }
}

특징:

  1. 스프링 컨테이너가 객체를 생성하고, 의존성을 자동으로 주입합니다.
  2. 개발자는 객체 생성과 의존성 주입의 번거로움에서 벗어나 비즈니스 로직에 집중할 수 있습니다.

5. DI의 장점

  1. 유연성: 의존성을 쉽게 교체할 수 있어 확장 가능성이 높습니다.
  2. 결합도 감소: 객체 간의 강한 결합을 제거하여 코드의 재사용성과 유지보수성을 높입니다.
  3. 테스트 용이성: Mock 객체를 주입하여 단위 테스트를 쉽게 수행할 수 있습니다.
  4. 코드 가독성: 각 클래스가 자신의 역할에만 집중하므로 코드가 간결해집니다.

6. 요약 표

항목 내용
정의 객체 간의 의존성을 외부에서 주입받아 결합도를 낮추고 유연성을 높이는 설계 방식.
핵심 개념 1. 객체가 직접 의존성을 생성하지 않고 외부에서 주입받음2. 결합도 감소 및 다형성 극대화
DI의 유형 생성자 주입, 세터 주입, 필드 주입
일상적인 예시 1. 가전제품과 콘센트2. 택배 서비스3. 식당의 셰프와 재료
장점 유연성 증가, 결합도 감소, 테스트 용이성, 코드 재사용성 및 가독성 향상
대표적인 활용 프레임워크 스프링 프레임워크 (DI 컨테이너를 통해 자동 의존성 주입)

7. 결론

DI는 객체 간의 의존성을 외부로 분리하여 개발자가 결합도를 낮추고, 코드를 더 유연하게 설계할 수 있도록 돕습니다. 이를 통해 객체는 자신의 역할에 집중하고, 의존성 관리의 복잡성은 외부 컨테이너(프레임워크)가 처리하게 됩니다. 일상적인 예시와 코드로 이해하면 DI는 "객체에 필요한 것을 외부에서 제공받아 유연하고 확장 가능한 구조를 만든다"는 개념으로 쉽게 이해할 수 있습니다.

 

**IOC(제어의 역전)**와 **DI(의존성 주입)**는 밀접하게 연관되어 있지만, 완전히 같은 말은 아닙니다. 둘의 관계는 다음과 같이 설명할 수 있습니다:

 

 

단일 책임 원칙 (Single Responsibility Principle - SRP)

  • 의미: 클래스는 단 하나의 책임만 가져야 한다. 하나의 클래스가 하나의 변경 이유만 가져야 한다.
  • 적용여부 : 변경 시 코드의 수정이 많이 않으면 제대로 SRP를 적용한 것
  • 장점:
    • 코드 변경 시 영향 범위 축소
    • 코드 가독성 증가 및 유지보수 용이
  • 코드 예시:
    // 잘못된 예: User 클래스가 데이터 관리와 이메일 발송 두 가지 책임을 가짐
    class User {
        public void saveToDatabase() {
            // DB 저장 로직
        }
        public void sendWelcomeEmail() {
            // 이메일 발송 로직
        }
    }
    
    // 개선된 예: 책임 분리
    class User {
        public void saveToDatabase() {
            // DB 저장 로직
        }
    }
    
    class EmailService {
        public void sendWelcomeEmail(User user) {
            // 이메일 발송 로직
        }
    }
    

 

개방-폐쇄 원칙 (Open-Closed Principle - OCP)

  • 의미: 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다. 새로운 기능 추가 시 기존 코드를 수정하지 않도록 설계해야 한다.
  • 확장하려면 변경이 필요하다고 생각하지만, 다형성을 생각해보자, 베이스가 되는 인터페이스를 토대로 새로운 클래스를 만들면 변경 없이도 확장이 가능하다.
  • 다만, 결국 객체를 변경하면 OCP 원칙을 지킬 수 없다. 이 경우 객체를 생성하고 연관 관계를 맺어주는 별도의 조립, 설정자가 필요하다.
더보기

OCP(개방-폐쇄 원칙)를 유지하려면 객체의 생성과 연관 관계 설정을 코드 내부가 아니라 별도의 설정자(조립자)에서 처리해야 합니다. 이렇게 하면 객체를 변경하지 않고도 동작을 유연하게 확장할 수 있습니다. 이를 이해하기 위한 예제를 단계별로 설명하겠습니다.


문제 상황: 객체 내부에서 다른 객체를 직접 생성

아래 코드는 OCP 원칙을 위반한 예입니다. 객체를 수정하지 않고는 다른 동작으로 확장할 수 없습니다.

class NotificationService {
    private EmailSender emailSender;

    public NotificationService() {
        this.emailSender = new EmailSender(); // 객체를 직접 생성
    }

    public void sendNotification(String message) {
        emailSender.send(message);
    }
}

class EmailSender {
    public void send(String message) {
        System.out.println("Sending Email: " + message);
    }
}

// 사용 코드
NotificationService notificationService = new NotificationService();
notificationService.sendNotification("Hello!");

문제점:

  • NotificationService가 EmailSender에 강하게 결합되어 있음.
  • 새로운 알림 수단(예: SmsSender, PushSender)을 추가하려면 NotificationService를 수정해야 함 → OCP 위반.

해결책: 별도의 조립자(설정자) 사용

객체를 직접 생성하지 않고 외부에서 주입받도록 변경하면 OCP 원칙을 지킬 수 있습니다. 아래는 이를 구현한 예제입니다.

1. 인터페이스를 사용해 역할(알림 수단) 정의

interface NotificationSender {
    void send(String message);
}

class EmailSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Sending Email: " + message);
    }
}

class SmsSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

2. NotificationService가 인터페이스에 의존하도록 수정

class NotificationService {
    private NotificationSender sender;

    // 생성자를 통해 객체를 주입받음
    public NotificationService(NotificationSender sender) {
        this.sender = sender;
    }

    public void sendNotification(String message) {
        sender.send(message);
    }
}

3. 조립자(설정자) 역할을 수행하는 클래스

조립자는 객체 생성과 의존성 주입을 담당합니다.

class NotificationConfigurator {
    public static NotificationService configure(String type) {
        NotificationSender sender;

        // 필요한 구현 객체를 결정하고 주입
        if ("email".equalsIgnoreCase(type)) {
            sender = new EmailSender();
        } else if ("sms".equalsIgnoreCase(type)) {
            sender = new SmsSender();
        } else {
            throw new IllegalArgumentException("Unknown notification type: " + type);
        }

        // NotificationService에 주입
        return new NotificationService(sender);
    }
}

4. 사용 코드

public class Main {
    public static void main(String[] args) {
        // 알림 방식 선택
        NotificationService notificationService = NotificationConfigurator.configure("sms");

        // 알림 전송
        notificationService.sendNotification("Hello via SMS!");
    }
}

설명

  1. 조립자(설정자) 역할: NotificationConfigurator 클래스가 객체 생성과 의존성 주입을 담당.
    • 이제 NotificationService는 구체적인 구현(EmailSender, SmsSender)을 몰라도 됨.
    • 새로운 알림 수단 추가 시, 기존 클래스(NotificationService)를 수정할 필요 없음.
      예: PushSender를 추가하려면, NotificationSender를 구현하고, NotificationConfigurator에만 추가하면 됨.
  2. OCP 원칙 준수:
    • NotificationService는 인터페이스(NotificationSender)에 의존하므로 확장이 용이.
    • 새로운 알림 방식 추가 시 기존 코드를 수정하지 않고도 확장이 가능.
  3. 스프링의 활용: 스프링 프레임워크는 이 조립자 역할을 DI(의존성 주입) 컨테이너가 대신 수행해 더욱 편리하게 다형성을 활용할 수 있도록 지원.

결론

객체의 생성과 의존성 주입을 별도의 조립자에서 처리하면 OCP 원칙을 지킬 수 있으며, 클래스의 확장성은 높아지고 변경 비용은 줄어듭니다. 이는 유지보수와 협업에 있어 큰 장점이 됩니다.

  • 장점:
    • 새로운 기능 추가 시 기존 코드 변경 최소화
    • 코드 안정성과 확장성 증가
  • 코드 예시:
    // 잘못된 예: 각 도형마다 개별 로직 추가 필요
    class AreaCalculator {
        public double calculateArea(Object shape) {
            if (shape instanceof Circle) {
                Circle circle = (Circle) shape;
                return Math.PI * circle.radius * circle.radius;
            } else if (shape instanceof Rectangle) {
                Rectangle rectangle = (Rectangle) shape;
                return rectangle.width * rectangle.height;
            }
            return 0;
        }
    }
    
    // 개선된 예: 다형성을 활용해 확장 가능
    interface Shape {
        double calculateArea();
    }
    
    class Circle implements Shape {
        double radius;
        public Circle(double radius) {
            this.radius = radius;
        }
        public double calculateArea() {
            return Math.PI * radius * radius;
        }
    }
    
    class Rectangle implements Shape {
        double width, height;
        public Rectangle(double width, double height) {
            this.width = width;
            this.height = height;
        }
        public double calculateArea() {
            return width * height;
        }
    }
    
    class AreaCalculator {
        public double calculateArea(Shape shape) {
            return shape.calculateArea();
        }
    }
    

 

리스코프 치환 원칙 (Liskov Substitution Principle - LSP)

  • 의미: 자식 클래스는 부모 클래스의 행위를 대체할 수 있어야 한다. 즉, 부모 클래스 타입의 객체를 자식 클래스 객체로 대체해도 동작해야 한다.
  • 장점:
    • 코드 재사용성 향상
    • 다형성 원칙 준수
  • 코드 예시:
// 잘못된 예: 자식 클래스가 부모 클래스의 기대를 위반
class Rectangle {
    int width, height;
    public void setWidth(int width) {
        this.width = width;
    }
    public void setHeight(int height) {
        this.height = height;
    }
    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = this.height = width;
    }
    @Override
    public void setHeight(int height) {
        this.width = this.height = height;
    }
}

// 사용 시 문제 발생
Rectangle rect = new Square();
rect.setWidth(5);
rect.setHeight(10);
System.out.println(rect.getArea()); // 잘못된 결과: 100 (정사각형 논리에 의해 동작)

 

 

인터페이스 분리 원칙 (Interface Segregation Principle - ISP)

  • 의미: 클라이언트는 사용하지 않는 메서드에 의존하지 않아야 한다. 인터페이스는 작고 구체적으로 분리해야 한다.
  • 장점:
    • 불필요한 의존성 제거
    • 인터페이스 변경 시 영향 최소화
  • 코드 예시:
    // 잘못된 예: 하나의 인터페이스에 불필요한 메서드 포함
    interface Printer {
        void print();
        void scan();
        void fax();
    }
    
    class BasicPrinter implements Printer {
        public void print() {
            // 프린트 기능 구현
        }
        public void scan() {
            throw new UnsupportedOperationException("Scan not supported");
        }
        public void fax() {
            throw new UnsupportedOperationException("Fax not supported");
        }
    }
    
    // 개선된 예: 인터페이스를 분리
    interface Printable {
        void print();
    }
    interface Scannable {
        void scan();
    }
    interface Faxable {
        void fax();
    }
    
    class BasicPrinter implements Printable {
        public void print() {
            // 프린트 기능 구현
        }
    }
    

 

의존 역전 원칙 (Dependency Inversion Principle - DIP)

  • 의미: 고수준 모듈(비즈니스 로직)은 저수준 모듈(구체적 구현)에 의존해서는 안 된다. 둘 다 추상화된 인터페이스에 의존해야 한다.
  • 단순히 말하면 구현(구체화) 클래스 말고 인터페이스(추상화)에 의존해서 코딩해라라는 것, 역할에 의존해야만 다형성을 잃지않고 확장이 잘되는 프로그램을 구성할 수 있다.
  • 장점:
    • 의존성 역전으로 모듈 간 결합도 감소
    • 유연성과 테스트 용이성 증가
  • 코드 예시:
    // 잘못된 예: 고수준 모듈이 저수준 모듈에 의존
    class MySQLDatabase {
        public void connect() {
            System.out.println("Connected to MySQL Database");
        }
    }
    
    class UserService {
        private MySQLDatabase database;
    
        public UserService() {
            this.database = new MySQLDatabase();
        }
    
        public void performDatabaseOperation() {
            database.connect();
        }
    }
    
    // 개선된 예: 인터페이스를 통한 의존성 역전
    interface Database {
        void connect();
    }
    
    class MySQLDatabase implements Database {
        public void connect() {
            System.out.println("Connected to MySQL Database");
        }
    }
    
    class PostgreSQLDatabase implements Database {
        public void connect() {
            System.out.println("Connected to PostgreSQL Database");
        }
    }
    
    class UserService {
        private Database database;
    
        public UserService(Database database) {
            this.database = database;
        }
    
        public void performDatabaseOperation() {
            database.connect();
        }
    }
    

 

요약 표

원칙  의미  장점  위반 시 문제
단일 책임 원칙 (SRP) 클래스는 하나의 책임만 가져야 함 변경 범위 축소, 가독성 및 유지보수성 향상 여러 이유로 클래스가 자주 변경되며, 코드가 복잡해짐
개방-폐쇄 원칙 (OCP) 확장에는 열려 있고, 수정에는 닫혀 있어야 함 안정성과 확장성 증가 새로운 기능 추가 시 기존 코드를 반복적으로 수정해야 함
리스코프 치환 원칙 (LSP) 자식 클래스가 부모 클래스의 행위를 대체할 수 있어야 함 재사용성 및 다형성 유지 자식 클래스가 부모 클래스의 기대를 깨뜨려 예상치 못한 결과 발생
인터페이스 분리 원칙 (ISP) 클라이언트는 필요하지 않은 메서드에 의존하지 않아야 함 불필요한 의존성 제거, 인터페이스 변경 영향 최소화 불필요한 메서드로 인해 코드가 복잡하고 유연성이 떨어짐
의존 역전 원칙 (DIP) 고수준 모듈은 저수준 모듈에 의존하지 말고, 인터페이스에 의존해야 함 결합도 감소, 테스트 용이성 증가 고수준 모듈이 구현체에 의존해 유연성과 테스트 가능성 상실

 

 

 

'Project > JAVA' 카테고리의 다른 글

[JAVA] 키오스크  (0) 2024.11.26
[JAVA] 계산기를 만들어보자!  (0) 2024.11.13

다른 부분은 저번 과제의 연속이라 잘 해결이 되었지만,

package org.example.expert.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.time.LocalDateTime;

@Slf4j
@Aspect
@Component
public class AdminAPILoggingAspect {

    private final ObjectMapper objectMapper;

    public AdminAPILoggingAspect(ObjectMapper objectMappers) {
        this.objectMapper = objectMappers;
    }

    @Around("execution(* org.example.expert.domain.comment.controller.CommentAdminController.*(..)) || " +
            "execution(* org.example.expert.domain.user.controller.UserAdminController.*(..))")
    public Object logAdminApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
        // 요청 및 응답 정보 로깅
        HttpServletRequest request = getHttpServletRequest();
        HttpServletResponse response = getHttpServletResponse();

        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);

        // 요청 본문 로깅
        String requestBody = new String(wrappedRequest.getContentAsByteArray());
        log.info("Admin API Request: Time={}, URL={}, UserId={}, Body={}",
                LocalDateTime.now(), request.getRequestURI(), request.getHeader("User-Id"), requestBody);

        Object result = joinPoint.proceed(); // 실제 메서드 실행

        // 응답 본문 로깅
        String responseBody = new String(wrappedResponse.getContentAsByteArray());
        log.info("Admin API Response: Time={}, URL={}, UserId={}, Body={}",
                LocalDateTime.now(), request.getRequestURI(), request.getHeader("User-Id"), responseBody);

        wrappedResponse.copyBodyToResponse(); // 응답 본문을 클라이언트에 전달

        return result;
    }

    private HttpServletRequest getHttpServletRequest() {
        // HttpServletRequest 가져오는 로직
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

    private HttpServletResponse getHttpServletResponse() {
        // HttpServletResponse 가져오는 로직
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
    }
}

 

현재 짰던 코드를 확인해 보았지만, 정확히는 파악이 안되어서 여러가지 찾아본 결과 나온 코드이다. 하지만 아직  정확하게는 판단이 안서서 하나하나 분석해 보려고 한다.

 

 


1. 클래스 정의

@Slf4j
@Aspect
@Component
public class AdminAPILoggingAspect {
  • @Slf4j: 로깅을 위해 사용하는 Lombok 애노테이션. log.info, log.error 같은 로깅 메서드를 제공.
  • @Aspect: 이 클래스가 AOP 역할을 수행하는 클래스라는 의미.
  • @Component: Spring이 이 클래스를 Bean으로 관리

2. 생성자

public AdminAPILoggingAspect(ObjectMapper objectMappers) {
    this.objectMapper = objectMappers;
}
  • ObjectMapper: JSON 처리를 위한 Jackson 라이브러리 객체야. 요청이나 응답 본문을 JSON으로 직렬화하거나 역직렬화할 때 사용.

3. AOP 핵심: @Around

@Around("execution(* org.example.expert.domain.comment.controller.CommentAdminController.*(..)) || " +
        "execution(* org.example.expert.domain.user.controller.UserAdminController.*(..))")
public Object logAdminApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
  • @Around: 특정 메서드 실행 전후에 로직을 실행하도록 설정.
  • 여기서는 CommentAdminController와 UserAdminController 클래스의 모든 메서드에 AOP를 적용
  • ProceedingJoinPoint joinPoint: 실제 대상 메서드를 호출할 수 있는 객체. AOP 적용 메서드 전후로 추가 로직을 실행 가능.

4. 요청 및 응답 객체 가져오기

HttpServletRequest request = getHttpServletRequest();
HttpServletResponse response = getHttpServletResponse();
  • 현재 요청과 응답 객체를 가져오는 거야. getHttpServletRequest()와 getHttpServletResponse()는 아래에서 따로 정의돼 있음.

5. 요청 로깅

ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
String requestBody = new String(wrappedRequest.getContentAsByteArray());
log.info("Admin API Request: Time={}, URL={}, UserId={}, Body={}",
        LocalDateTime.now(), request.getRequestURI(), request.getHeader("User-Id"), requestBody);
  • 요청 본문(Body)을 읽기 위해 ContentCachingRequestWrapper를 사용.
  • getContentAsByteArray()로 요청 데이터를 가져온 뒤 로그로 출력.
  • 로그에 포함된 정보:
    • 요청 시간 (LocalDateTime.now()).
    • 요청 URL (request.getRequestURI()).
    • 요청자 ID (request.getHeader("User-Id")).
    • 요청 본문 내용 (requestBody).

6. 대상 메서드 실행

Object result = joinPoint.proceed();
  • AOP가 적용된 원래 메서드를 실행.
  • 여기선 CommentAdminController나 UserAdminController의 메서드가 실행됨.

7. 응답 로깅

ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
String responseBody = new String(wrappedResponse.getContentAsByteArray());
log.info("Admin API Response: Time={}, URL={}, UserId={}, Body={}",
        LocalDateTime.now(), request.getRequestURI(), request.getHeader("User-Id"), responseBody);
  • 응답 본문을 로깅. ContentCachingResponseWrapper로 응답 데이터를 읽음.
  • 로그에 포함된 정보:
    • 응답 시간.
    • 요청 URL.
    • 요청자 ID.
    • 응답 본문 내용.

8. 응답 복원

wrappedResponse.copyBodyToResponse();
  • ContentCachingResponseWrapper가 데이터를 읽으면 스트림이 닫히기 때문에, 복원해서 클라이언트에 다시 보냄.

9. 요청/응답 객체 가져오기

private HttpServletRequest getHttpServletRequest() {
    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}

private HttpServletResponse getHttpServletResponse() {
    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
}
  • RequestContextHolder를 사용해서 현재 요청(Request)과 응답(Response) 객체를 가져옴
  • AOP는 컨트롤러 메서드 앞뒤에서 동작하니까, 여기서 현재 요청 정보를 얻음

코드 전체 흐름

  1. 특정 관리자 API가 호출되면, AOP가 작동.
  2. 요청 정보를 가져와서 로깅.
  3. 원래 메서드를 실행 (joinPoint.proceed()).
  4. 응답 정보를 로깅.
  5. 요청과 응답 모두 클라이언트로 전달.

 

'Project' 카테고리의 다른 글

ElasticSearch  (0) 2025.02.05
 

GitHub - sparta-sixsense/newsfeed

Contribute to sparta-sixsense/newsfeed development by creating an account on GitHub.

github.com

 

 

 

 

KEEP

📌 현재 만족하고 계속 이어가고 싶은 = 유지할 부분

  • 나를 위한 커밋이 아니라 팀원을 위한 커밋을 하기 위해 메시지 내용을 어떻게 쓸 지 고민해
  • 프로젝트를 진행하면서 활용하면 좋을 것 같은 다양한 로직과 기술을 사용하고 공유함 (코드 컨벤션, 소스패키지 구조 시각화 등)
  • 정규표현식과 regxp 를 사용해 입력패턴을 적용해 봄
  • 팀의 분위기를 긍정적으로 이끌어가기 위해 노력함
  • 깃 컨벤션을 미리 정해놓은 부분이 좋았음
  • 코드의 안정성을 위해 공통 기능을 우선적으로 준비하고 프로젝트를 시작했던 점이 추후 도움이 되었음
  • 비즈니스 로직 간소화
  • 검증 로직은 별도 메서드로 이원화
  • 팀원들과 화목한 관계 추구 노력

 

 

PROBLEM

📌 불편하게 느꼈고 수정하고 싶은 부분 = 문제였던 부분

  • 선행학습이 부족해 다른 팀원이 사용하는 기술을 이해하지 못함
  • 계획을 세우지 않아 시간을 효율적으로 관리하지 못하고, 개인 학습 시간을 전혀 가지지 못함
  • 깃허브 커멋 컨벤션이 엄격하게 지켜지지 않음
  • 테이블 설계시, 처음에 고려치 못한 부분에 대해 우선순위에서 밀렸다는 이유로 반영 못함
  • 다른 브랜치에 푸쉬하는 실수를 저질렀음
  • 실제 애플리케이션 배포까지 경험해보자라는 목표로 프로젝트에 임했지만 AWS에 대한 이해 부족으로 배포 단계까지 가지 못함
  • 예외 처리를 다소 복잡함. 어느 수준까지 분기할 건지 고민 필요
  • API Url 설계할 때 조금 더 RESTful하게 만들 필요가 있음. url path만 보고 어떤 역할을 하는지 이해하기 쉽지 않은 API 다수
  • 소규모 프로젝트라, 패키지 구조를 [controller, service, repository, domain, dto ...] 이런 식으로 만들었는데, 생각보다 DTO 클래스가 많아 조금만 더 프로젝트 규모가 커지면 유지보수 불가. domain 단위 패키지 구조로 마이그레이션 고려

 

 

TRY

📌 문제 해결을 위해 실행 가능한 것들 = 앞으로의 목표

  • 혼자 해보기엔 어려웠던 기능을 팀원들과 협업하면서 시도해보기
  •  
  • 계획표를 꼭 세우고, 개인 학습 시간 챙기기
  • 다음에는 컨벤션을 더 확실히 정하고 프로젝트를 시작하기
  • 타 팀의 잘한 점을 최대한 흡수하기, 새로운 인사이트를 얻기
  • 우선은 필요 기능을 모두 구현하고, 디테일을 살리기
  • 데이터베이스 설계나 sql에 대해 공부를 더 해야겠다는 생각이 들었음
  • 기능을 왜 쓰는지, 어떻게 쓰는건지 정확히 파악하는 것이 가장 중요하다는 것을 알게됨
  • 다음 프로젝트에서는 간트 차트를 작성해 보고자 함
  • 테스트 코드를 작성
  • 더미데이터를 정합성 있게 제조
  • 테스트 코드가 github에서 자동으로 빌드되고, 애플리케이션이 자동으로 서버에 배포되는 단계까지 적용
  • AWS, Docker, 쿠버네티스, 엘라스틱 서치, 레디스, 카프카 적용
  • 깃에 대한 상세 학습

 

[ 참고 자료 ]

 

팀 문화의 탄생 | 우아한형제들 기술블로그

안녕하세요, 우아한형제들 상품시스템팀의 손권남입니다. 가끔씩 저는 우리팀의 팀 문화에 대한 질문을 받곤 합니다. 그때마다 매번 단편적인 답을 드리곤 하면서 한 번 정도 우리의 팀문화를

techblog.woowahan.com

 

 

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

Spring Querydsl 과제 회고  (1) 2025.01.27
[Spring] IOC & DI  (0) 2025.01.23
일정표를 만들어 보자! 업데이트!  (2) 2024.12.19
일정표를 만들어 보자!  (2) 2024.12.09
[ Spring ] 쇼핑몰 프로젝트 회고  (0) 2024.11.25

트러블 슈팅

1. 배경

  • 웹 개발 숙련에 입문하였다.
  • 사용 기술은 jpa와 쿠키,세션이 추가되었다.
  • 웹 개발의 기초에서 진행하였던 일정표 프로젝트를 개량하는 것이 발제였다.

 

2. 발단

  • 처음은 아니였던 만큼 머리속에 웹 개발의 전재라인이 떠올랐던 점이 크게 작용했다.
  • 확실히 저번보다는 편하게 진행이 가능했다는 점이 조금은 성장했다는 것을 느낄 수 있었다.
  • 하지만 문제가 없지는 않았다.
  • 프론트와 함께 쓰는것을 목표로 했다보니, 자연스럽게 프로젝트도 프론트를 구성했었는데, 튜터님의 조언이 백엔드에 치중해서 코드를 작성해보라는 내용이 있다는 것을 너무 늦게 파악했다..
  • 코드적인 문제로는, 중간에 프론트에서 데이터를 한번에 2가지 servlet으로 전송하려했는데 그게 좀처람 잘 되지 않았다.
  • 그 외에도 세션을 사용하려다가 생기는 오류가 많아 애를 먹었다.

 

3. 전개&위기

  • 여전히 문제는 저 2개였다. controller와 service, 전에는 controller와 repository에 모든 기능을 다 넣었지만 이번에는 service를 추가해보는 것이 좋을 것 같다는 조언에 의해 나눠서 처리했다.
  • 이번에는 확실히 처리하는 파일과 저장하는 파일이 따로 존재하다보니 코드의 수정에서 굉장히 편하다는 것을 느낄 수 있었고, 일단 코드를 굳이 엄한데에서 안 찾아도 되었다는 점이 굉장히 편했다.
  • 다만, 세션의 filter의 경우 처음하는 부분이다보니 많이 해매였던 것 같다. 
  • 코드 자체가 인증, 인가라고는 하지만 막상 해보기 전까지는 어떤 방식의 구현인지 이해가 정확히되지 않았다는 점이 컸던 것 같다.

 

5. 절정

  • 일단 처음에는 받았던 강의 내용을 그대로 사용해 보다가 gpt를 통해 업그래드를 진행해 보았다. 내용은 간단히 받은 세션과 내용의 일치를 통한 간단한 절차를 구현했지만, 뭔가 많이 부족해보인다.
  • 대부분의 문제의 경우 bean이나 다양한 파트에서 자동으로 지원해 준다는 점에서 간편하게 구현이 가능했고 처음에 많이 해메이던 부분은 한번 하고나니 확실히 익숙해지는 것이 느껴졌다.

 

6. 결말

  • 이번에는 계속 집 수리로 인해 시간을 너무 많이 잃었던 점이 코드를 작성하지 못한 큰 이유가 되었다.
  • 강의도 정말 일단 가능한곳까지는 해보자는 생각으로 추가학습과 주말도 사용했지만, 생각보다 기공분들이 너무 많이 찾아오셨고 체크를 담당하고 설명을 듣고 부모님께 전달하는 시간이 너무 길었다.
  • 앞으로도 계속 이럴 수 없다는 생각이 먼저들었다. 
  • 동생과 분담하는 방식으로 일정을 조절해야지, 안그러면 팀프로젝트에서도 민폐를 끼칠 것 같다.
  • 코드는 단순히 있는 코드를 변형하는 방식이라 jpa자체는 엄청 힘들지는 않았지만, 처음해보는 세션로그인은 조금 해메였던 것 같다.
  • 처음에 걱정하던 부분은 머리속에 개발 로드맵이 어느정도 구성되고 나니 좀 불안이 덜어진 것 같다.
  • 다만 postman과 테스트코드가 없다는 점에서 앞으로도 문제는 해결할 필요가 있어보인다.
  • 프론트를 일부러 같이 가져가려고 했지만, 그렇게 좋은 방법은 아니였다는 조언이 있었던 만큼, 차라리 프론트를 만들 시간에 다른 백엔드를 좀 더 가공하려 하는 방식으로 바꿔야할 것 같다.

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

Spring Querydsl 과제 회고  (1) 2025.01.27
[Spring] IOC & DI  (0) 2025.01.23
스프링 NEWSFEED 협업 프로젝트  (1) 2024.12.27
일정표를 만들어 보자!  (2) 2024.12.09
[ Spring ] 쇼핑몰 프로젝트 회고  (0) 2024.11.25

트러블 슈팅

1. 배경

  • 드디어 웹 개발의 기초에 입문하였다.
  • 사용 기술은 spring, jsp, jdbc, mysql이다.
  • 웹 개발의 기초를 위한 일정표를 만드는 발제를 진행하였다.

2. 발단

  • 처음부터 끝까지 문제가 상당히 많았다.
  • 데이터의 타입 정의는 문제가 없었다
  • 예외처리에서 예외처리가 무한으로 재귀되는 문제가 있어 확인해 보았다.
  • 매핑 형식에 대한 오류가 등장하였다.
  • 데이터 로드 형식, 범위에 대한 문제가 발생하였다.
  • SQL문의 경우 오랜만에 사용하다보니, 조금 익숙한 감이 떨어졌다.

3. 전개&위기

1. Service

  • 이 파트는 단순히 클라이언트의 요청을 처리하는 파트였어서 큰 문제는 없었다.
  • 다만 지금 보니 파일이나 폴더명이 service로 되어있어야했다.

2. exception

  • 위와 같이 대분의 오류를 판단하고 메시지 형태로 페이지에 띄우는 코드를 작성했다.
  • 다만 여기서 문제가 있었던 것이, 맨 처음에는 페이지로 연동을 안 시켜주어서 에러를 띄우기 시작하였다.

3. controller

  • 여기 파트의 경우 데이터를 불러오고 처리하는 직접적인 메인에 가까운 코드였다.
  • 여기서는 다양한 문제가 발생하였는데, 데이터를 외래키를 사용하기 위해 테이블을 2개로 나누고나니, 요청에서 문제가 다수 발생하였다.
  • 그 이외에도 post,get만 사용하였지만 다양한 방식을 사용하지 못했다는 점도 문제로 잡혔다.
  • 가장 큰 문제는 사실 위의 문제보다는 2개가 나타났었다
  • 하나는, 그저 시연에 있던 코드를 정확히 이해하지 못하고 치기만 했다는 점에서 이해가 부족해 보인다.
  • 둘, 여기는 서버라서 그런건지 인터럽트가 안걸린다. 이게 엄청난 시간을 딜레이 시킨 요인이 되기 시작했다.

4. repository

  • 데이터를 불러오는 형식이나 양, 직접적인 jdbc를 이용한 sql문 처리를 담당했다.
  • 여기서 가장 큰 문제는 sql일것이라 생각했으나, 이미 해본 적이 있던 부분이라 차라리 할만했다.
  • 오히려 외래키가 들어오고 나서 부터 생기는 join 문제가 발목을 잡았다.
  • 가장 큰 문제였던 점은, mysql에서는 직접 데이터를 불러오고 삭제하지만 여기서는 다른 java코드에서 요청하다보니 정확히 어디서 요청하고 어디서 문제가 난 것인지 몰랐다는 점이 너무 시간을 잡아먹었다.

  • 여기는 딱히 막히는 부분은 없었지만, 저 위의 태그의 사용방식을 정확히 알지 못한게 좀 시간을 잡아먹었다.

5. 절정

1. controller

  • 처음에는 잘 몰랐지만, 하면서 controller의 역활은 생각보다 복잡하면서도 명확하다는 것을 파악할 수 있었다.
  • 우선 필요한 데이터를 수집하고, 데이터를 통해 문제를 처리한다. 혹시모를 예외를 처리하면서 다른 코드와 가장 밀접하게 많이 연동되는 곳이라는 것을 파악할 수 있었다.
  • 과제의 내용에 외래키를 꼭 사용하는 것이 포함되어 있었다보니, 원래의 코드를 수정하여 postRepository를 2가지 테이블에 같이 걸어 데이터를 제공받는 코드로 작성하였다.
  • 예외는 exception 테이블에 있는 GlobalExceptionHandler에서 처리할 수 있도록 예외를 해당 클래스와 연동된 페이지로 페이지 이동을 걸어두었다.
  • 모든 클래스는 요청하는 것이 어떤 데이터인지 형식인지의 차이는 있었지만 위의 형태를 유지하도록 코드를 구성했다.

 

2. repository

  • 여기서는 조인외에는 거의 다 데이터베이스 자체적인 오류로 인해 문제가 많이 발생하였다.
  • 외래키를 첨가하고 나서부터, user_id라는 외래키를 직접 입력을 받는 형식을 처음에는 사용하였으나,
  • 사실 옆 테이블의 기본키라는 것을 깨닫고 그냥 옆에서 데이터를 불러오는 형식으로 바꾸려 했더니 기존 코드와 충돌이 어마무시하게 일어났다.
  • user_id가 입력이 안되어서 무결성 오류로 데이터가 저장이 안되는 경우부터, 데이터가 한 테이블만 저장되고 다른 테이블에는 외래키로 인해 저장이 안되는 등 여러 문제가 있었다.
  • 결론적으로 대부분 외래키에 대한 무결성 보존 오류였다. 이 점은 항상 주의하는 수 밖에 없을 것 같다.

 

3. exception

  • 여기서 의존성 문제가 가장 많이 등장했던 것 같다. 
  • 다만 이 부분은 코딩적인 문제라기보단, 지식의 문제라 많이 하다보면 괜찮을 것 같다.

 

6. 결말

  • 이번 코드에서 걱정했던 클린 코드의 문제가 조금씩 드러나고 있는 것 같다.
  • DB,서버,클라 이렇게 3개가 통신하다보니 더 코드가 복잡하고 읽어내기 힘들었다고 생각한다.
  • 왜 사람들이 폴더를 정확히 나눠서 코드를 짜라는지 알 수 있었고 앞으로도 처음부터 잘 배치하는 것을 목표로 해야할 것 같다는 생각이 들었다.
  • 이번 코드하면서 테스트를 하면서 하진 않았다. 다만, 서버의 경우 반드시 테스트를 통해서 점검을 해야하는 점을 생각하면 추후 반드시 한번은 해봐야겠다.
  • 위의 에러들이 가장 알기 어려웠지만, 지금와서 보면 체계를 잘 잡는게 중요한 것 같다.
  • 웹 개발의 로드맵을 머리에 저장해 두면 앞으로 이런 문제는 좀 덜 생길 것 같다.
  • 하면서 가장 어려웠던게 어디서부터 어떤 순서로 만들어야하는지 감이 전혀 안왔기 때문이다.
  • 각 프로그램들이 하는 일에 대한 정의가 머리에서 흐릿하게만 떠올라서 배치하는 것 조차 어려웠기 때문이다.
  • 그리고 다양한 개념이 갑자기 쓰이다보니, 또 머리속에서 개념이 흔들리고 있다. 이 부분도 함께 채워야할 것 같다.

 

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

Spring Querydsl 과제 회고  (1) 2025.01.27
[Spring] IOC & DI  (0) 2025.01.23
스프링 NEWSFEED 협업 프로젝트  (1) 2024.12.27
일정표를 만들어 보자! 업데이트!  (2) 2024.12.19
[ Spring ] 쇼핑몰 프로젝트 회고  (0) 2024.11.25

트러블 슈팅

 

1. 배경

  • 전에 했던 과제를 반복해서 좀더 현업에서 진행하는 것과 같은 형식의 코딩을 진행하였다.
  • 기본적으로 자바 기능을 재 학습하는 목적으로 사용된 프로젝트였다.

2. 발단

  • 기본적으로 눈에 띄는 에러는 없었다.
  • 다만 코드가 요구사항이 길어짐으로서 복잡해지는 점이 문제가 되었다.

3. 전개

 

  • 기본적으로 코드가 길어지고 클래스가 늘면서 위와 같이 한 폴더에 7개의 파일이 생성되게 되었다.
  • 많은 수는 아닐지 모르지만 가장 중요한 점은, 이 파일이 100개가 넘어가면 한 폴더에서 다 처리하기엔 가시성이 너무 떨어질 수 있다는 점이다.
  • 두번째는 코드의 조건문의 형식으로 인해 가시성이 많이 떨어져 보인다.
  • 실제로 객체 지향적인 코딩을 진행했지만, 이것만으로는 부족한지 내용에 수정이 필요해 보인다.

 

4. 위기

  • 역시 폴더를 임의로 바꾸니 에러가 나기 시작했다.
  • 내용은 패키지의 지정이 되어 있지 않아 생기는 오류였다.

5. 절정

  • 검색해 보니 단순히 패키지 지정을 해주지 않아서 생기는 오류였다.
  • 실제로 코드가 복잡해지고 나면 이런 저장소 위치나 수정 일시 등의 정보는 자주 기록하는 것이 좋다.

 

6. 결말

  • 기본적으로 사용했던 기능을 복습하는 코드였다보니 어려운 응용은 없었지만, 아직 파일의 분리와 가시성에 대한 부분이 자연스럽게 진행되지 못한 다는 것을 깨달았다.
  • 이는 유지보수에도 관련이 있지만 AWS와 같은 외부 요인의 업데이트로 발생하는 오류를 처리하는데도 도움을 줄 수 있다.
  • SOLID원칙 객체지향 프로그래밍 등, 클린 코드를 위한 기법은 다양하게 많고 실제로 모든 상황에 앞선 기술들이 적용되진 못한다는 것을 감안하면, 앞으로의 코딩은 수행 - 검수 - 수정의 과정을 거쳐야할 것 같다.
  • 즉, 스승이 필요하다..

 

7. 회고

  • 확실히 전 보다 감이 오는 부분은 객체지향인 것 같다.
  • 실제로 이 코드가 완전한 객체 지향을 수행한 것은 아니겠지만, 적어도 왜 객체지향이 필요하고 왜 요구사항이 중요한지는 파악할 수 있었다.
  • 정확한 설계가 있어야 추후 문제가 없다. 완벽한 설계는 무리일지언정, 적어도 필요한 기능과 구현에 대한 정리는 머리속에 있어야한다는 것
  • 이게 완료되면 다양한 설계 패턴과 디자인 패턴을 이용하여 최대한 코드를 알아보기 쉽게 짤 수 있어야한다.
  • 이 과정에서 어노테이션과 주석이 정말 중요한 역활을 할 수 있다고 생각하게 되었다.
  • 주석이라는 것이, 혼자 코딩할때는 대강 기능에 대한 설명을 단촐하게 서술하는 경우가 많았지만, 이렇게하면 추후 알아보기 힘들어질 수 있다.
  • 실제로 혼자서 2000줄정도 넘어가면 코드를 짜던 나 조차도 이게 뭐지 하는 경우가 많이 있었다.
  • 스프링은 특히 어노테이션으로 이루어져 가시성도 좋은 만큼 많이 활용할 수 있으면 장점이 클 것 같았다.
  • 과거의 프로젝트도 이런 식으로 진행했으면 아마, 지금에 와서 다시 수정해서 사용해볼 수도 있었을 것 같다.
  • 다만 셀레니움처럼 외부의 데이터가 자주 변경되는 경우 대응이 힘든 코드는 대체 어떻게 관리를 하는건지 여전히 모르겠다는 생각이 든다.
  • 다음에는 이 유지보수 방법에 대해 깊게 공부하는 편이 좋을 것 같다.

 

 

'Project > JAVA' 카테고리의 다른 글

[JAVA] SOLID 원칙  (1) 2025.01.23
[JAVA] 계산기를 만들어보자!  (0) 2024.11.13

★요구사항 분석과 기초 설계의 중요성

  • 코드가 정말 길어지고 나니, 처음에 설계를 잘못했을 때 너무너무 큰 대가를 치르게 되었다.
  • 맨 처음에 요구사항을 정확히 분석하고 완전한 설계를 해두어야 추후 문제가 없다.

백엔드 프로젝트의 준비물

  • 자바, 스프링, JPA, 서버 사이드 템플릿(Thymeleaf), DB

백엔드 프로젝트의 구성

  • resources : 프론트엔드 + 애플리케이션 설정 파일

  • Java : 백엔드 전반

엔티티( Entity) & 빈(Bean)

  • 1. 엔티티(Entity)
    • 정의: 엔티티는 주로 데이터베이스 테이블에 매핑되는 객체입니다. 데이터베이스에서 특정 테이블과 일대일 매핑을 하여, 해당 테이블의 데이터를 객체로 다룰 수 있게 해줍니다.
    • 용도: 엔티티는 주로 **JPA(Java Persistence API)**와 함께 사용되며, 데이터베이스의 데이터를 객체지향적으로 처리하는 역할을 합니다.
      • 예를 들어, @Entity 어노테이션을 사용하여 JPA 엔티티 클래스를 정의하면, 이 클래스는 데이터베이스 테이블과 연관됩니다.
      • 엔티티 클래스는 객체 상태와 데이터베이스 상태 간의 매핑을 담당합니다. 즉, 객체의 필드 값이 데이터베이스의 열(column)과 매핑됩니다.
    • 특징:
      • 데이터베이스와 연결되어 있으며, 데이터의 저장, 조회, 수정, 삭제 등의 작업을 할 수 있습니다.
      • 일반적으로 @Entity 어노테이션으로 클래스가 엔티티로 선언됩니다.
      • 엔티티는 보통 영속성 관리의 대상이 되어 JPA나 Hibernate가 객체의 생명 주기를 관리합니다.
    2. 빈(Bean)
    • 정의: **빈(Bean)**은 Spring에서 사용되는 객체로, Spring IoC(제어의 역전) 컨테이너에 의해 관리되는 객체입니다. Spring에서는 Bean을 관리하고 생성하는 방식으로 객체 간의 의존성을 관리합니다.
    • 용도: 빈은 애플리케이션의 구성 요소로, 객체의 생성, 의존성 주입(DI), 생명주기 관리 등을 Spring 컨테이너가 담당합니다.
      • Spring 컨테이너에 의해 관리되므로, 애플리케이션에서 필요한 객체를 명시적으로 생성하거나 관리할 필요 없이 Spring이 자동으로 빈을 관리하고 주입합니다.
      • 빈은 애플리케이션의 비즈니스 로직을 담당하는 서비스나 컴포넌트 클래스일 수 있습니다.
    • 특징:
      • 빈은 @Component, @Service, @Repository, @Controller 등의 어노테이션을 사용하여 선언할 수 있습니다.
      • Spring IoC 컨테이너는 이러한 빈들을 자동으로 관리하고, 의존성 주입을 통해 필요한 곳에 제공해 줍니다.

영속성

 

영속성의 개념

영속성은 객체 지향 프로그래밍(OOP)에서 메모리 상의 객체가 영구 저장소(주로 데이터베이스)에 저장되는 과정과 관련이 있습니다. 즉, 프로그램의 실행이 종료되더라도 데이터가 계속해서 유지될 수 있게 하는 것을 의미합니다.

JPA에서의 영속성

**JPA(Java Persistence API)**에서 영속성은 객체가 데이터베이스 테이블과 매핑되어 데이터베이스에 저장되거나, 수정되고, 삭제되는 과정을 관리하는 기능입니다. JPA는 자바 애플리케이션에서 데이터베이스와의 상호작용을 객체 지향적으로 처리할 수 있게 해주는 API입니다.

JPA 영속성의 주요 개념

JPA에서는 **엔티티 객체(Entity Object)**를 사용하여 데이터를 저장하고, 이 엔티티 객체들이 **영속성 컨텍스트(Persistence Context)**라는 환경 내에서 관리됩니다. 영속성 컨텍스트는 영속성 상태를 관리하는 메모리 내의 저장소로, JPA가 객체와 관계형 데이터베이스 간의 매핑을 관리합니다.

 

 

JPA를 사용한 이유

 

  • 객체 중심 설계를 지원해 코드의 생산성과 유지보수성을 높임.
  • SQL 대신 메서드 호출로 데이터베이스 작업을 처리하여 간결한 코드 작성 가능.
  • 영속성 컨텍스트를 통해 1차 캐시, 지연 로딩 등 최적화된 데이터 관리 제공.
  • JPQL로 객체 중심의 쿼리 작성 가능.
  • 다양한 데이터베이스와 독립적인 표준 API로 확장성과 호환성 보장.
  • 자동화된 데이터베이스 매핑으로 개발 시간 단축.
  • 변경 감지 및 트랜잭션 관리 기능으로 안정적인 데이터 처리가능.

 

JPA가 어노테이션인 이유

JPA에서 어노테이션을 사용하는 이유는 객체-관계 매핑(ORM, Object-Relational Mapping)을 간편하고 선언적으로 처리하기 위함입니다. 즉, 객체 지향적인 코드에서 관계형 데이터베이스의 데이터를 관리할 때 발생하는 여러 번거로운 작업들을 쉽게 처리할 수 있도록 도와줍니다. 여기서 중요한 점은 JPA가 객체와 데이터베이스 테이블 간의 매핑을 어노테이션으로 지정함으로써 코드가 더 간결하고 유지보수가 용이해진다는 점이다.

 

테스트 환경 생성

  • 단순한 코드의 경우 그냥 수정할 수 있지만, 복잡한 코드가 될 수록 수정에 신중해야한다. 그래서 test코드를 작성하여 컴파일 해보면서 문제가 없는지, 버그는 없는지, 제대로 동작하는지 테스트 하는 것이 중요하다.
  • 문론 테스트 코드도 유지보수를 해야하기 때문에 의미있는 테스트 케이스를 작성해야한다.
  • 서버에 올리는 코드는 에러가 없어야한다. 즉, 이 과정은 정말 중요한 과정이다.

 

@Query와 Querydsl

  • JPA에서 SQL 쿼리를 작성하는 2가지 대표적인 방법이다.
  • 쿼리는 데이터베이스에서 원하는 데이터를 조회하거나 조작하기 위한 명령어를 말합니다.

1. @Query

@Query는 JPA 리포지토리에서 제공하는 어노테이션을 사용하여 JPQL(Java Persistence Query Language) 또는 네이티브 SQL 쿼리를 작성할 수 있게 해줍니다.

장점

  • 간단하고 직관적: SQL이나 JPQL을 직접 작성하기 때문에, 쿼리가 간단하고 직관적입니다. SQL에 익숙한 개발자라면 바로 사용할 수 있습니다.
  • 구현이 쉬움: 쿼리를 메소드에 어노테이션으로 추가할 수 있어서 코드 작성이 간단하고 빠릅니다.
  • 복잡한 쿼리도 가능: JPQL이나 네이티브 SQL로 복잡한 쿼리를 작성할 수 있기 때문에, SQL에 대한 제어가 가능합니다.
  • 쿼리 작성 시 강력한 최적화 가능: SQL을 직접 작성할 수 있어 데이터베이스에 최적화된 쿼리를 작성할 수 있습니다.

단점

  • 쿼리 재사용이 어려움: 쿼리를 여러 곳에서 사용할 경우, 쿼리가 중복될 수 있어 유지보수가 어렵습니다.
  • 쿼리 복잡도 증가: 복잡한 동적 쿼리를 작성할 때, 여러 조건을 동적으로 처리하는 것이 번거로울 수 있습니다.
  • 타입 안정성 부족: 문자열로 쿼리를 작성하기 때문에 컴파일 타임에 쿼리가 잘못 작성되었는지 확인할 수 없습니다. 즉, 쿼리 문법이나 컬럼명이 잘못된 경우 런타임에 오류가 발생할 수 있습니다.
  • 컴파일 시점에서 에러가 안나온다.

2. Querydsl

Querydsl은 자바 코드로 SQL 쿼리를 작성할 수 있게 해주는 라이브러리입니다. 타입 안전성을 제공하며, 쿼리를 빌더 방식으로 동적으로 생성할 수 있습니다.

장점

  • 타입 안전성: 컴파일 타임에 쿼리를 검증할 수 있어, 잘못된 필드명이나 잘못된 쿼리 문법에 대해 오류를 미리 확인할 수 있습니다.
  • 동적 쿼리 작성 용이: 여러 조건을 기반으로 동적으로 쿼리를 생성할 수 있어 복잡한 쿼리를 유연하게 작성할 수 있습니다.
  • 쿼리 재사용 용이: Querydsl을 사용하면 쿼리를 객체 지향적으로 작성할 수 있어, 코드 재사용과 유지보수가 용이합니다.
  • 자동 완성 지원: IDE에서 자동 완성 기능을 제공하므로, 쿼리 작성 시 실수를 줄일 수 있습니다.

단점

  • 설정과 초기 학습 곡선: Querydsl을 프로젝트에 통합하는 데 다소 시간이 걸리며, 초기 설정이 복잡할 수 있습니다.
  • 추가 의존성: Querydsl을 사용하려면 추가 라이브러리나 빌드 설정이 필요하므로 프로젝트에 의존성이 늘어날 수 있습니다.
  • 가독성 저하: 자바 코드로 SQL을 작성하기 때문에, 단순한 쿼리에서는 오히려 가독성이 떨어질 수 있습니다. 복잡한 쿼리를 작성할 때 코드가 길어질 수 있습니다.

결론

  • @Query단순하고 빠른 개발을 위해 좋습니다. 작은 프로젝트나 복잡하지 않은 쿼리에서는 매우 유용하며, 기존 SQL 문법에 익숙한 개발자에게 적합합니다.
  • Querydsl동적이고 복잡한 쿼리를 작성할 때 유리합니다. 타입 안전성과 재사용성을 제공하므로, 대규모 프로젝트나 복잡한 비즈니스 로직을 구현할 때 더 효과적입니다.

 

Thymeleaf를 사용한 이유

 

+ Thymeleaf는 Java 기반의 서버 사이드 템플릿 엔진으로, 주로 Spring Framework와 함께 사용됩니다. HTML, XML, JavaScript, CSS 등의 파일을 동적으로 렌더링 (데이터를 가공)할 수 있는 템플릿 시스템입니다.

= 컨트롤러에서 데이터 처리 -> 타임리프가 템플릿화 -> 스프링이 데이터 전송

 

1. 자연스러운 템플릿(Natural Templates):

  • 타임리프: HTML 파일을 템플릿 엔진 없이 열어도 유효한 HTML로 보이고, 동적 데이터가 서버에서 렌더링됩니다. 디자이너와 개발자가 협업하기 용이합니다.
  • 다른 엔진 (예: Freemarker, Velocity): HTML 구조와 템플릿 문법이 혼합되어 있기 때문에, 템플릿 파일을 직접 브라우저에서 열면 정상적인 HTML로 보이지 않습니다. 또한, HTML 파일을 비전문가와 협업하기 어려울 수 있습니다.

2. 강력한 표현식 언어:

  • 타임리프: 조건문, 반복문, 텍스트 출력 등 다양한 표현식 기능을 제공하며, HTML 내에서 쉽게 사용할 수 있습니다. 특히, 속성 값을 처리할 때 유연하고 직관적입니다.
  • 다른 엔진: 다른 템플릿 엔진들도 조건문, 반복문 등을 제공하지만, 타임리프처럼 HTML 속성 내에서 직접 사용할 수 있는 방식은 아닙니다. 예를 들어, Freemarker는 <#if>나 <#list>와 같은 태그를 사용해야 하며, Velocity는 $!{}와 같은 특수 문법을 사용합니다. 타임리프는 이러한 문법을 덜 사용하고, HTML 내에서 자연스럽게 사용할 수 있습니다.

3. Spring과의 통합성:

  • 타임리프: Spring과 매우 잘 통합되며, Spring Boot에서 기본적으로 지원됩니다. Spring의 다양한 기능(모델 객체, 메시지 처리, 국제화 등)과 자연스럽게 결합할 수 있습니다.
  • 다른 엔진: FreemarkerVelocity도 Spring과 통합할 수 있지만, 타임리프만큼 자연스럽고 쉽지는 않습니다. 특히 Spring Boot와의 통합에 있어 타임리프는 기본 지원이므로 설정이 간단합니다.

4. 확장 가능성:

  • 타임리프: 커스텀 디얼리티브나 프로세서를 만들어 확장이 가능하며, 새로운 태그나 속성을 쉽게 추가할 수 있습니다.
  • 다른 엔진: FreemarkerVelocity는 확장성도 있지만, 타임리프는 확장이 용이하고, 사용자가 직접 커스텀 태그를 추가할 때 좀 더 직관적이고 깔끔한 방식으로 구현할 수 있습니다.

5. 강력한 디버깅 기능:

  • 타임리프: 템플릿 렌더링 시 오류 메시지를 상세히 제공하여 디버깅을 용이하게 합니다. 오류를 명확하게 알려줘서 개발자가 쉽게 문제를 찾을 수 있습니다.
  • 다른 엔진: FreemarkerVelocity는 디버깅 기능이 있지만, 타임리프처럼 직관적이고 명확한 오류 메시지를 제공하는 데는 부족할 수 있습니다.

6. 모듈화된 레이아웃 지원:

  • 타임리프: 레이아웃을 모듈화할 수 있는 기능(th:replace, th:include)을 제공하여 코드 중복을 줄이고 유지보수성을 높입니다.
  • 다른 엔진: FreemarkerVelocity도 레이아웃을 모듈화할 수 있는 기능을 제공하지만, 타임리프의 th:replace, th:include 같은 방식은 더 직관적이고, HTML 속성으로 처리할 수 있어 코드 작성이 용이합니다.

7. 보안:

  • 타임리프: 기본적으로 HTML 이스케이프 기능이 제공되어 XSS(교차 사이트 스크립팅)와 같은 보안 문제를 예방할 수 있습니다.
  • 다른 엔진: FreemarkerVelocity도 보안 기능을 제공하지만, 기본적으로 HTML 이스케이프 처리가 되지 않는 경우가 많습니다. 보안을 강화하려면 개발자가 별도로 처리해야 할 수 있습니다.

 

DTO 클래스

  • Data Transfer Object의 약자로, 엔티티 객체를 그대로 사용하지 않고 데이터를 포장하여 원하는 값만 전달하기 위해 필요한 데이터 전송 담당 클래스
  • View Layer와 DB Layer의 분리를 위해서, 엔티티 객체의 변경을 막기 위해서, 데이터베이스 설계 외부 노출을 방지하기 위해 등 보안과 효율적인 면을 위해서 사용하게 된다.

 

부트스트랩

  • HTML, CSS, JavaScript로 구성된 오픈 소스 프론트엔드 프레임워크로, 웹 페이지나 웹 애플리케이션을 보다 빠르고 효율적으로 개발할 수 있도록 돕는 도구
  • 타인이 만든 웹 디자인을 공유할 수 있는 페이지
  • 백엔드는 프론트를 하는 경우도 많지만 일단은 웹 디자이너 역활은 못하기에... 이걸로 대체했다..

 

스프링 시큐리티

  • 단순히 로그인 보안에 사용했다.
  • 다만 여러 기능이 있는 만큼, 더 갈고 닦을 필요가 있어보인다.
  • 내용은 단순히 검문 시스템 정도로 이해하면 편한 것 같다.
  • 요청이 들어오면 요청에 대한 검사와 처리를 수행하는 메서드로 구성된다.
  • 관리자와 비 관리자의 보안 관리는 다르게 들어가는데, 가장 큰 이유는 여기서 데이터 정보를 어디까지 허용할지 지정하기 때문

 

매핑

  • 클라 - 서버 - DB는 서로 데이터를 보내거나 불러올때 계속 매핑을 해야한다.
  • 간단히 서로 언어가 다르니 번역해야한다는 것, [ 사과 - 애플 - 링고 ] 이런식으로 언어가 다르다 보니 서로 번역하는 작업이 필요한데 이게 매핑이다.
  • 매핑은 처음에는 언제 사용해야 하나 라는 생각이 들었는데, 그냥 클라, 서버, db 각각 내부에서 사용하는 변수, 상수가 아니면 싹다 매핑을 해야한다. = 서로 뭘 주고 받으면 매핑해야한다...

AJAX 요청에서 401 Unauthorized 상태 코드를 반환하는 이유

일반적으로 AJAX 요청은 페이지 리로드 없이 서버와 비동기적으로 데이터를 주고받기 위해 사용되기 때문(웹 페이지 일부만 업데이트해서 리로드 없어도 가능함)입니다. 즉, 브라우저가 페이지를 새로고침하지 않고도 서버와 데이터를 주고받을 수 있는 방식입니다. 이때, 인증되지 않은 상태일 경우 어떻게 처리할지에 대한 특별한 요구사항이 있을 수 있습니다.

 

이유:

  1. 페이지 리디렉션이 불가능하기 때문에:
    • 일반적인 HTTP 요청에서는 인증이 필요할 경우, 사용자를 로그인 페이지로 리디렉션할 수 있습니다.
    • 하지만 AJAX 요청은 페이지 리로드를 하지 않고 데이터를 주고받는 방식이기 때문에, 리디렉션을 처리할 수 없습니다.
    • 만약 HTTP 401 Unauthorized 상태 코드 대신 로그인 페이지로 리디렉션하면, AJAX 요청을 보낸 클라이언트는 리디렉션을 처리하지 못하고, 예상하지 못한 동작이 발생할 수 있습니다. 예를 들어, 클라이언트는 로그인 페이지로 리디렉션된 후, 이를 다시 처리하기 위해 페이지를 새로고침해야 할 수 있습니다.
  2. AJAX 응답의 형식:
    • AJAX 요청은 비동기적으로 서버와 통신하므로, 클라이언트는 서버로부터 데이터를 받아와서 페이지 내에서 처리합니다.
    • 서버에서 401 Unauthorized 상태 코드를 반환하면, 클라이언트 측에서 이를 처리하고, 사용자에게 적절한 오류 메시지를 표시하는 등 후속 작업을 할 수 있습니다.
    • 리디렉션을 대신하여, 클라이언트는 이 상태 코드를 처리하여 사용자를 로그인 페이지로 유도할 수 있습니다. 예를 들어, alert()으로 "로그인 필요" 메시지를 띄우거나, 로그인 페이지로 리디렉션하는 방식으로 처리할 수 있습니다.

+ 리디렉션(Redirection)은 웹에서 사용자가 특정 요청을 했을 때, 서버가 다른 URL로 사용자를 자동으로 보내는 방법을 의미합니다.

 

연관 관계 매핑 종류

  • 매핑이 당연히 1:1이라 생각할 수 있지만, 아래처럼 일대다의 형태를 띄기도 한다. 이럴경우 리스트로 받아온다.
  • 이 연관 관계 매핑을 잘 생각하고 매핑해야한다. 정보 이상하게 불러오면 처리고 뭐고 다 망해간다.

 

1. 단방향 관계 (Unidirectional)

  • 한 엔티티에서 다른 엔티티를 참조하는 형태입니다.
  • 다른 방향으로의 접근은 불가능하고, 하나의 엔티티에서만 연관된 엔티티에 접근할 수 있습니다.

2. 양방향 관계 (Bidirectional)

  • 두 엔티티가 서로를 참조하는 형태입니다.
  • 두 방향으로의 접근이 가능하며, 이를 설정하기 위해 mappedBy를 사용하여 연관 관계를 설정합니다.

 

연관 관계 매핑 종류

1. One-to-One (1:1 관계)

  • 한 엔티티가 다른 엔티티와 1:1 관계를 맺는 경우입니다.
  • 예시: 한 사람은 하나의 여권만 가질 수 있다.

2. One-to-Many (1:N 관계)

  • 한 엔티티가 여러 엔티티를 참조하는 경우입니다.
  • 예시: 하나의 게시글에는 여러 댓글이 있을 수 있다.

3. Many-to-One (N:1 관계)

  • 여러 엔티티가 한 엔티티를 참조하는 경우입니다.
  • 예시: 여러 댓글은 하나의 게시글에 속한다.

4. Many-to-Many (N:M 관계)

  • 여러 엔티티가 서로 다대다 관계를 가지는 경우입니다.
  • 예시: 여러 학생은 여러 과목을 수강할 수 있다.

 

영속성 전이

  • 엔티티의 상태를 변경할 때 해당 엔티티와 연관된 엔티티의 상태 변화를 전파시키는 옵션을 말한다.
  • 이때 부모는 One에 해당하고 자식은 Many에 해당한다.
  • 예시로 Order엔티티 삭제 - 연관된 다른 엔티티 삭제 / Order엔티티를 저장할때 포함된 다른 엔티티도 저장되는 경우
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)

 

 

고아 객체의 삭제

부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 말하는 것으로, 영속성 전이 기능과 같이 사용하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.

  • 다만 고아 객체는 위의 영속성 전이와 같이 다른 곳에 영향을 줄 수 있다는 점에서 삭제에 유의해야한다.
  • 고아 객체가 하나의 참조만 하고 있다면 삭제가 가능하지만, 그게 아니라면 어떤 변수가 발생할지 모르기에 삭제가 불가능 하다.
  • 예시 : orphanRemoval = true라고 하면 만약 주문 항목(부모) 삭제시, 항목들도 데이터 베이스에서 삭제된다.

 

지연 로딩

일반적으로 로딩을 시전하면 DB 특정 항목의 모든 데이터를 불러온다. 이러한 대참사를 막기 위해 있는 것이 지연 로딩 방식이다. 지연 로딩은 데이터를 실제로 필요할 때까지 로딩을 지연시키는 방식이다. 즉 관련된 엔티티나 컬렉션이 실제로 사용되기 전까지는 DB에서 불러오지 않는다는 것.

 

Auditing을 이용한 엔티티 공통 속성 공통화

Auditing = Spring Data Jpa에서 제공하는 엔티티가 저장 or 수정될 때 자동으로 등록일,수정일,등록자,수정자를 입력해주는 기능을 말한다. 실제 영어 뜻도 '감시하다'의 의미를 가지고 있다. 

  • 문론 위의 정보 중 필요한 정보만 자동 저장할 수 있게 만들 수 있다.

 

애플리케이션 실행 상태에서 테이블 수정

일반적으로 소스를 작성하면서 애프릴케이션을 재실행하면 테이블을 삭제하고 다시 만들기 때문에 코드를 실행한 이후에 저장된 데이터는 삭제된다. 이 과정을 해결하기 위해 application.prperties의 dll-auto 속성을 validate로 변경하면 애플리케이션 실행 시점에 테이블을 삭제한 후 재생성하지 않으면 엔티티와 테이블이 매핑이 정상적으로 되어 있다.

= 디버깅 중에 테이블 내용을 수정할 수 있다.

 

Hibernate의 ddl-auto 속성

hibernate.hbm2ddl.auto는 Spring Data JPA에서 데이터베이스 테이블을 자동으로 관리하는 방법을 지정하는 설정입니다. 애플리케이션을 실행할 때, 이 설정에 따라 데이터베이스의 테이블이 어떻게 처리될지 결정됩니다.

여기서 validate, update, create, create-drop 값들이 중요한데, 각각 어떤 동작을 하는지에 대해 간단히 설명해볼게요.

문제의 핵심:

  1. 테이블 삭제 후 재생성:
    • 기본적으로 **create**나 **create-drop**을 설정하면, 애플리케이션이 시작될 때 데이터베이스의 테이블이 삭제되고 새로 생성됩니다. 이때 기존에 저장된 데이터는 모두 삭제됩니다.
  2. validate로 설정하면?:
    • hibernate.hbm2ddl.auto=validate로 설정하면, 애플리케이션이 시작될 때 기존 테이블을 삭제하거나 새로 만들지 않습니다.
    • 대신, 데이터베이스의 테이블 구조가 애플리케이션의 엔티티 클래스와 잘 매핑되어 있는지 확인합니다. 만약 테이블 구조가 엔티티 클래스와 일치하지 않으면 애플리케이션이 예외를 발생시키며 실행되지 않습니다.
    • 즉, 엔티티와 데이터베이스 테이블이 잘 매핑되어 있다면 테이블은 변경되지 않고, 데이터도 유지됩니다.
spring.jpa.hibernate.ddl-auto= create

-----------------------------------------

spring.jpa.hibernate.ddl-auto= validate

 

화면이 너무 복잡하고 얽힌 데이터가 많으면

Vue.js 프레임워크를 사용해보자

  • 데이터가 변하면 해당 데이터를 보여주는 영역에 뷰도 자동으로 바뀌고, 데이터가 변하는 걸 감시하고 있다가 이벤트를 발생하기 쉽다.
  • Vue.js는 반응형(Reactivity) 시스템을 제공합니다. 이 시스템 덕분에 데이터가 변경되면 해당 데이터를 사용하는 UI(뷰)가 자동으로 업데이트됩니다.
  • 를 들어, 사용자가 데이터를 수정하거나 서버에서 데이터를 받아오면 Vue.js는 이 데이터를 감시하고 있다가, 데이터가 변경될 때마다 관련된 UI 요소를 자동으로 갱신합니다.
  • 이는 상태 관리UI 갱신을 자동으로 해주기 때문에, 개발자는 UI를 직접 수정할 필요 없이 데이터만 수정하면 화면이 자동으로 반영됩니다.
  • 그러니까 변형의 자동 인지와, 관련된 UI를 자동으로 업데이트 해준다는 것

 

 

네트워크 생명주기 모델

 

1. 네트워크 생명주기 (요청-응답 흐름)

위 코드가 작동하는 전체 네트워크 생명주기는 아래와 같이 설명됩니다.

① 요청 (Request)

  1. 클라이언트(브라우저 또는 JavaScript)는 서버로 요청을 보냅니다.
    • URL: /order (POST 요청)
    • 데이터: JSON ({ "itemId": 1, "count": 2 })
    • 방식: AJAX를 통해 비동기적으로 전달.

② 처리 (Processing)

  1. 컨트롤러 (OrderController)
    • @PostMapping("/order") 메서드가 요청을 받습니다.
    • @RequestBody를 통해 JSON 데이터를 Java 객체(OrderDto)로 매핑.
    • 유효성 검사 결과(BindingResult)를 확인하고, 서비스 계층 호출.
  2. 서비스 (OrderService)
    • 비즈니스 로직을 처리하며, 데이터베이스 접근 및 주문 처리.
    • 처리 결과로 주문 ID를 반환.

③ 응답 (Response)

  1. 컨트롤러가 처리 결과를 JSON 형태로 반환:
    • ResponseEntity<Long>로 반환된 주문 ID를 JSON으로 변환.
    • 클라이언트는 AJAX의 성공 콜백 함수에서 이를 처리.

④ 클라이언트 반응

  1. 클라이언트는 서버에서 받은 JSON 데이터를 활용하여 화면 업데이트:
    • 주문 완료 메시지 표시.
    • 주문 ID와 관련된 정보를 화면에 출력.

2. 요청-응답 간 상호작용 요약

  • 클라이언트 → 서버:
    AJAX로 JSON 데이터를 서버에 전달 (@RequestBody).
  • 서버 → 클라이언트:
    처리 결과를 JSON 형태로 반환 (@ResponseBody).

3. AJAX 사용의 장점

  • 페이지 새로고침 없이 실시간 업데이트 가능.
  • 사용자 경험(UX)을 크게 개선.
  • 요청-응답 간 데이터 크기를 줄여 효율성 증가.

4. 코드 흐름에 AJAX가 통합되는 구조

  1. 프론트엔드에서 주문 요청 시 AJAX를 호출.
  2. 백엔드 컨트롤러와 서비스 계층이 데이터 처리.
  3. 응답 데이터를 클라이언트가 받아 사용자 인터페이스(UI) 업데이트.

+ json = 클라이언트, 서버 통신시 사용되는 경량형 데이터 형식 (map 구조)

+ AJAX = 클라이언트 ,서버 통신시 비동기 통신을 가능하게 해주는 웹 기술 (프로토콜 아님)

 

프론트와의 통신

 

  • 맵핑은 기본적으로 서버에서 진행한 다음 db나 클라에게 보낸다.
  • $(".custom-file-input"):
    • 의미: 이 코드는 jQuery를 사용하여 클래스 이름이 custom-file-input인 HTML 요소들을 선택하는 코드입니다.
    • $(".custom-file-input")는 HTML 문서 내에서 클래스가 custom-file-input인 모든 요소를 찾아 조작하거나 스타일을 적용할 수 있게 합니다.
    • 예를 들어, 파일 선택 버튼을 커스터마이즈할 때 자주 사용됩니다.
  • $("#searchBtn"):
    • 의미: 이 코드는 jQuery를 사용하여 아이디가 searchBtn인 HTML 요소를 선택하는 코드입니다.
    • $("#searchBtn")는 id="searchBtn"인 요소를 선택하여 해당 요소를 조작할 수 있습니다. 일반적으로 버튼 클릭 이벤트를 다룰 때 사용됩니다.
    • 예를 들어, 검색 버튼을 클릭했을 때 검색 기능을 활성화하는 용도로 사용될 수 있습니다.
  • th:href="@{/css/layout1.css}":
    • 의미: 이 코드는 Thymeleaf 템플릿 엔진을 사용하여 HTML 링크 태그의 href 속성을 동적으로 설정하는 코드입니다.
    • th:href는 Thymeleaf의 특수 속성으로, 페이지가 렌더링될 때 href 속성을 해당 URL로 변환합니다. @{}는 해당 URL을 프로젝트의 루트 경로를 기준으로 계산해줍니다.
    • 예를 들어, @{/css/layout1.css}는 /css/layout1.css라는 경로로 CSS 파일을 참조하는 링크를 만듭니다. 이 경로는 애플리케이션의 컨텍스트 경로를 자동으로 고려하여 적절하게 해석됩니다.
  • th:
    • 의미: th는 Thymeleaf 템플릿 엔진에서 사용하는 네임스페이스의 접두어입니다. Thymeleaf는 HTML을 렌더링할 때 동적 데이터를 삽입하거나 처리할 수 있게 해주는 서버 사이드 템플릿 엔진입니다.
    • th 접두어는 Thymeleaf 특수 속성을 나타내며, 예를 들어 th:href, th:text, th:src와 같은 속성을 사용할 수 있습니다. 이는 HTML 속성에 동적 값을 삽입하거나 변환할 때 사용됩니다.
    예시:
    • th:text="${message}": message 변수에 담긴 값을 텍스트로 삽입합니다.
    • th:src="@{${imagePath}}": imagePath 변수를 경로로 변환하여 이미지 소스를 설정합니다.

 

 

 


+추가사항+

 
 

1. 트랜잭션 관리 (Transaction Management)

  • 트랜잭션은 데이터베이스에서 일련의 작업들이 일관성 있게 처리되도록 보장하는 메커니즘입니다. 예를 들어, 여러 데이터베이스 작업이 하나의 큰 작업을 구성할 때, 작업 중 하나가 실패하면 전체 작업이 롤백되어야 합니다.
  • Spring에서는 @Transactional 어노테이션을 사용하여 트랜잭션을 관리할 수 있습니다. 트랜잭션이 적용되면 메서드 실행이 끝나기 전에 모든 DB 작업이 성공적으로 처리되거나 롤백됩니다.
  • 트랜잭션 전파, 격리 수준, 예외 처리 등도 고려해야 할 중요한 부분입니다.

2. 캐시 (Cache)

  • 캐싱은 애플리케이션 성능을 최적화하는 중요한 기법입니다. 자주 조회되는 데이터를 메모리에 저장해두고, 데이터베이스 접근을 최소화하는 방법입니다.
  • Spring에서는 @Cacheable, @CacheEvict 등의 어노테이션을 활용해 쉽게 캐시를 적용할 수 있습니다.
  • Redis나 Ehcache와 같은 외부 캐시 시스템을 연동하여 성능을 크게 향상시킬 수 있습니다.

3. API 설계 (RESTful API)

  • RESTful API는 HTTP를 통해 자원을 CRUD(Create, Read, Update, Delete) 방식으로 처리하는 API 설계 방법론입니다.
  • RESTful API 설계 시 **HTTP 메서드(GET, POST, PUT, DELETE 등)**의 사용법과 상태 코드(예: 200 OK, 404 Not Found 등)를 정확하게 이해하고 활용하는 것이 중요합니다.
  • Spring에서는 @RestController를 사용하여 REST API를 쉽게 구현할 수 있습니다.

4. 로깅 (Logging)

  • 로깅은 애플리케이션의 상태를 기록하고, 오류나 문제 발생 시 이를 추적하는 데 중요한 역할을 합니다.
  • SLF4JLogback을 사용하여 로그를 관리하고, 로그의 레벨을 적절히 설정하여 개발, 테스트, 프로덕션 환경에서 로그를 효과적으로 사용할 수 있습니다.
  • 로그 추적을 통해 성능 병목이나 오류를 빠르게 파악할 수 있습니다.

5. 보안 (Security)

  • Spring Security를 사용하여 로그인, 인증, 권한 관리 등 다양한 보안 기능을 구현할 수 있습니다.
  • CSRF, XSS, 세션 관리 등과 같은 웹 애플리케이션 보안 문제를 고려해야 합니다.
  • 인증과 권한 처리를 통해 사용자가 수행할 수 있는 작업을 제한하고, 민감한 정보에 대한 접근을 제어합니다.

6. 배포와 CI/CD (Continuous Integration / Continuous Deployment)

  • 애플리케이션을 CI/CD 파이프라인을 통해 자동으로 빌드하고 배포하는 시스템을 구축하면, 배포 시 오류를 최소화하고 효율성을 높일 수 있습니다.
  • Jenkins, GitHub Actions, GitLab CI 등을 사용하여 자동화된 빌드 및 배포 프로세스를 설정할 수 있습니다.

7. 단위 테스트(Unit Testing)

  • JUnit과 Mockito를 사용하여 단위 테스트를 작성하면, 코드 변경이 기존 기능에 영향을 미치지 않는지 확인할 수 있습니다.
  • 테스트 주도 개발(TDD, Test-Driven Development)을 통해 코드 품질을 높이고, 버그를 사전에 예방할 수 있습니다.

8. API 문서화 (API Documentation)

  • Swagger와 같은 도구를 사용하여 API 문서를 자동으로 생성할 수 있습니다.
  • Springfox 또는 Springdoc OpenAPI를 활용하여 API 문서화가 용이하며, 팀원들과의 협업 시 API 사용법을 명확하게 전달할 수 있습니다.

9. 비동기 처리 (Asynchronous Processing)

  • 비동기 작업 처리는 시간 소모적인 작업을 별도의 쓰레드에서 처리하여 애플리케이션의 응답성을 높이는 데 유용합니다.
  • Spring에서는 @Async 어노테이션을 사용하여 비동기 메서드를 처리할 수 있습니다.

10. 로그인 및 세션 관리 (Session Management)

  • 세션 관리는 사용자가 로그인 상태를 유지하도록 하는 기능입니다. JWT (JSON Web Token)와 같은 토큰 기반 인증 시스템을 활용하여 분산 환경에서도 세션을 관리할 수 있습니다.
  • 세션 스토리지와 쿠키를 잘 활용하여 사용자의 인증 상태를 안전하게 관리하는 방법을 이해해야 합니다.

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

Spring Querydsl 과제 회고  (1) 2025.01.27
[Spring] IOC & DI  (0) 2025.01.23
스프링 NEWSFEED 협업 프로젝트  (1) 2024.12.27
일정표를 만들어 보자! 업데이트!  (2) 2024.12.19
일정표를 만들어 보자!  (2) 2024.12.09

+ Recent posts