🙋수정 전 코드

// 요청 구성
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
    .bucket(bucketName)
    .key(filePath)
    .build();

 

AWS S3에 파일 업로드 하기 전 요청을 구성하는 코드이다.

구성 후 S3에 업로드 된 파일의 경로, 즉 URL을 응답하는식으로 업로드 API를 작성하고 URL에 접근했는데

내가 원하는 건 URL에 접속했을 때 브라우저에 바로 이미지가 보여지는 것이었지만

해당 URL은 이미지 다운로드만 되었다.

 

🙋 수정 후 코드 (Content-Type 설정 추가)

// 요청 구성
    PutObjectRequest putObjectRequest = PutObjectRequest.builder()
        .bucket(bucketName)
        .key(filePath)
        .contentType(file.getContentType()) // 업로드 시 파일의 Content-Type 설정 *  파일을 이미지로 인식 -> 다운로드 대신 웹 페이지에 표시
        .build();

 

💡 이전에 코드가 다운로드 됐던 이유 :

파일이 어떤 종류인지 인식하지 못하기때문에 다운로드할 파일로 처리됐던 것이다.

따라서 Content-Type 설정을 통해 파일의 형식이 명확히 설정되어 브라우저가 해당 URL에 접근할 때 다운로드 하지않고

바로 화면에 표시할 수 있게 된다.

 

 

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

✏️ 내가 작성한 코드 

def solution(clothes):
    dic = {}
    sum = 1
    
    # 의상 종류별로 의상이름 리스트 만들어서 넣어주기
    for cloth in clothes:
        if cloth[1] not in dic:
            dic[cloth[1]] = []
        dic[cloth[1]].append(cloth[0])
    
    # 의상 이름 개수 세기 위해 리스트로 만들기
    dicValList = list(dic.values())
    
    for i in range (len(dicValList)):
        # 의상 종류에서 아무것도 안입는 경우의 수 1을 추가
        # 예) 안경, 선글라스, 아무것도 안 입기
        sum *= len(dicValList[i]) + 1
        
    return sum - 1 # 전부 입지 않은 경우의 수 빼야함

 

✏️ 참고  (sum에서 곱할 때 +1 하면서 곱하는 이유?)

선택지를 

dicValList[0] : 동그란 안경, 검정 선글라스, 아무것도 안입기
dicValList[1] : 파란색 티셔츠, 아무것도 안입기
dicValList[2] : 청바지, 아무것도 안입기
dicValList[3] : 긴 코트, 아무것도 안입기

이런식으로 구성한다고 생각하며 +1를 하였다.

 

그 이유는 예를 들어 의상을 구성할 때

동그란 안경 +  파란색 티셔츠

동그란 안경 + 아무것도 안입기

이런식으로 구성이 가능하기때문에 1개(동그란 안경)만 의상을 착용한 경우도 나타낼 수 있다.

단, 아무것도 안입기 +  아무것도 안입기 +  아무것도 안입기 +  아무것도 안입기 도 sum에 포함되기 때문에 

이건 빼줘야하므로 -1을 하면서 return한다.

🙋 @Cacheable 캐싱 안 먹는 이유? (Spring AOP)

Spring에서 @Cacheable 써서 캐싱할 때,

같은 클래스 안에서 메서드 부르면 AOP가 안 먹힌다. 그 이유는 Spring AOP는 프록시 객체로 동작하는데, 클래스 내부에서 자기 자신을 호출하면 프록시가 아니라 해당 메서드를 불러버리기 때문이다.

프록시는 외부에서 해당 메서드가 호출될 때만 동작한다.

 

  @Service
  public class userService {
  
      public testResponse test(Long id) {
      ...
      	  // user 정보 얻기
          User user = getUser(id);
      ...
      }

	  // 캐싱 적용
      @Cacheable(value = "userCache", key = "#root.args[0]")
      public User getUser(id){
        return userRepository.findById(id)
      }
 
 }

 

 

✏️ 해결방법 첫번째, 서비스 분리

캐싱할 메서드를 다른 서비스(UserCacheService)로 빼버린다.

 

@Service
public class UserCacheService {
    @Autowired
    private UserRepository userRepository;
    
    @Cacheable(value = "userCache", key = "#root.args[0]")
    public User getUser(Long id){
        return userRepository.findById(id);
    }
}

 

✏️ 해결방법 두번째, 자기 자신을 빈으로 주입

서비스 나누기 싫다면? 자기 자신을 @Autowired을 통해 Bean으로 주입한다.

그러면 자기 자신 호출할 때도 프록시를 거치니까 AOP 적용되고 캐싱도 잘 된다.

 

@Service
public class UserService {

    @Autowired
    private UserService self;  // 자기 자신을 주입

    public TestResponse test(Long id) {
        User user = self.getUser(id);  // 프록시를 거쳐서 호출
    }

    @Cacheable(value = "userCache", key = "#root.args[0]")
    public User getUser(Long id){
        return userRepository.findById(id);
    }
}

 

💡 Redis로 캐시 확인하는 방법

해당 프로젝트는 Redis를 캐시 저장소로 설정했다. (Redis는 Docker로 실행)

따라서 캐싱이 잘 적용됐는지 확인하기 위해 Redis CLI 사용.

Redis Docker 컨테이너 실행하고 Spring에서 Redis와 연동됐다는 가정하에,

 

1) Redis CLI 접속

Docker로 실행 중인 Redis에 접속하려면, 터미널에서 Redis CLI에 들어가서 명령어를 입력할 수 있다.

docker exec -it [redis 컨테이너 이름] redis-cli

 

2) 캐시된 키 확인

현재 Redis에 저장된 모든 캐시 키를 볼 수 있다.

keys *

 

 

🙋 도커 컨테이너 실행 시 에러 발생할 때? (포트 충돌인 경우)

 

✅ 1. 재시작

 

간혹 Docker 자체의 문제로 인해 포트가 해제되지 않는 경우가 있다.

이럴 땐 도커를 재시작 해보면 된다.

 

docker restart [docker_서비스_이름]

 

  2. 현재 포트 점유 확인

 

특정 포트번호를 사용하는 프로그램이나 다른 컨테이너가 있는지 확인해야 한다.

아래 명령어를 통해 확인 가능하다.

 

netstat -ano | findstr : 포트번호

 

출력된 결과에서 PID (프로세스 ID)를 확인한 후,

해당 PID를 사용하고 있는 프로그램을 종료하거나, 다른 포트를 사용할 수 있도록 설정을 시도해본다.

🙋  Enter Password: 비밀번호를 잊어버렸을 때?

docker exec -it [컨테이너 이름] bash
mysql -u root -p
Enter Password:

 

✏️ 아래 명령어를 실행해보자. PASSWORD를 바로 확인할 수 있다.

docker exec [컨테이너 이름] printenv | findstr MYSQL_ROOT_PASSWORD

 

💡 참고 - 도커 설치부터 MySQL 실행까지

 

MySQL Docker 컨테이너 접속 (Docker을 활용한 개발 환경 구성)

✏️ Docker란? (참고:https://greenring.tistory.com/46) ✏️ Docker을 활용하여 MySQL에 접속하는 이유 : 새로운 환경마다 MySQL을 설치 안해줘도됨. MySQL 이미지를 내려받아 컨테이너를 구성하면 효율적. ✏️

greenring.tistory.com

 

🙋 Reflection API란?

구체적인 클래스 타입을 알지 못해도 메서드, 타입, 변수 등 해당 클래스의 정보에 접근할 수 있게 해주는 Java API이다.

public class Member {
	private String name;
	private int age;

	public Member(String name, int age) {
		this.name = name;
		this.age = age;
	}

	public void memberTest() {
		System.out.println("test입니다.");
	}
}

 

public static void main(String[] args) {
    Object obj = new Member("kim", 10);
    obj.memberTest();    // 에러 발생
}

 

obj는 Object로 타입이 결정(java는 컴파일 시점에 타입이 결정되므로 여기서 obj는 Object로 타입이 결정된다.)됐으므로

Object 클래스 메서드, 변수들만 사용이 가능해서 Member클래스의 memberTest 사용 시 에러가 발생한다.

(Member 클래스의 구체적 타입을 모르기 때문이다.)

 

이때 Member클래스의 정보에 접근 가능하게 해주는 것이 Reflection API이다.

(* 사용법 참고 https://www.baeldung.com/java-reflection)

이런 동작이 가능한 이유는 JVM가 실행되면서 코드가 컴파일러에 의해 바이트 코드로 변환되고, static영역에 저장된다.

Reflection API는 이 static 영역에 저장된 정보를 활용하는 것이다.

 

그러나, 우리가 실제로 코드를 작성할 때는 구체적인 클래스를 모르는 일은 거의 없다.

따라서 애플리케이션 개발보다 프레임워크나 라이브러리에서 많이 사용된다.

 

🙋 Spring Data JPA에서 Entity에 기본 생성자가 필요한 이유?

필요한 이유는 동적으로 객체 생성 시 Reflection API 를 활용하기 때문이다.

JPA는 DB 값을 객체 필드에 주입할 때 기본 생성자로 객체를 생성한 후 Reflection API를 사용하여 값을 매핑한다.

때문에 기본 생성자가 없다면 Reflection은 해당 객체를 생성 할 수 없기 때문에 JPA의 Entity에는 기본 생성자가 필요하다.

 

👀 Reflection API :

🙋 기본 생성자를 'public', 'protected'로 선언해야하는 이유?

Entity 기본 생성자의 접근 제어자는 private로 선언할 수 없다.

그 이유는, JPA의 Entity 조회 방식 중 하나인 '지연로딩' '프록시 객체'와 관련이 있다.

지연로딩 시 사용되는 프록시 객체는 원본 Entity를 상속해서 만들기때문에 원본 Entity의 기본생성자가 private일 수 없는것이다. 

 

👀 지연로딩, 즉시로딩 :

더보기

지연로딩 (Lazy Loading) :

지연 로딩은 연관된 엔티티를 실제로 사용할 때 쿼리를 실행하는 방식이다.

부모 엔티티를 조회할 때 연관된 자식 엔티티는 초기에는 로딩되지 않고, 필요한 순간에 쿼리를 실행하여 데이터를 가져온다.

이를 통해 불필요한 데이터 로딩을 최소화할 수 있다.

(*프록시 객체 : 실제 사용될 때까지 조회를 지연하기위해 가짜 객체가 필요하다. 이때 프록시 객체를 사용한다.)

 

즉시로딩 (Eager Loading):

부모 엔티티를 조회할 때 연관된 자식 엔티티도 함께 조회된다.

이 경우, 쿼리는 부모 엔티티를 조회할 때 실행된다.

 

 

🙋 JPA에서 제공하는 여러가지 쿼리 생성 방법을 알아보자.

 

1. 쿼리 메소드(Query Methods) :

  • 메소드 이름을 통해 JPQL 쿼리를 생성하는 방법
  • Spring Data JPA에서 자동으로 메소드 이름을 분석하여 JPQL을 생성

메서드 이름을 특정한 규칙에 맞게 작성하면, Spring Data JPA는 해당 메서드를 실행할 때 자동으로 쿼리를 생성하여 데이터베이스에 전달한다.

public interface PostRepository extends JpaRepository<Post, Long> {
    
    // 제목에 특정 키워드가 포함된 게시글 검색
    List<Post> findByTitleContaining(String keyword);

예를 들어, findByTitleContaining 메서드는 다음과 같은 역할을 수행한다.

  • findBy : 메서드 이름의 시작 부분으로, "findBy" 다음에 오는 속성명으로 쿼리를 생성한다.
  • Title : 엔터티 클래스의 속성 중 하나이다. 여기서는 title이라는 속성을 의미한다.
  • Containing : 속성에 대한 조건을 나타내며, 해당 속성이 특정 키워드를 포함하는지를 확인한다.

따라서, findByTitleContaining은 "title" 속성에서 특정 키워드를 포함하는 게시글을 찾는 쿼리를 생성한다.

Spring Data JPA는 이 메서드를 실행할 때, 자동으로 쿼리를 생성하고 실행한다.

예를 들어, 메서드를 호출할 때 findByTitleContaining("Java")와 같이 호출하면, Spring Data JPA는 "title에 'Java'를 포함하는 게시글을 찾아라"는 쿼리를 생성하고 실행한다.

이렇게 메서드 이름을 통해 동적으로 쿼리를 생성할 수 있어서 편리하게 사용할 수 있다.

 

2. JPQL (Java Persistence Query Language) : 

 

쿼리 메소드는 특정한 규칙을 따르며, 이를 통해 자주 사용되는 CRUD 기능을 편리하게 제공한다.

하지만 복잡한 쿼리를 표현하기에는 한계가 있을 수 있다. 이런 경우에는 @Query 어노테이션을 사용하여 직접 JPQL을 작성한다.

  • 객체지향 쿼리 언어로, 엔티티 객체를 대상으로 쿼리를 작성할 수 있다.
  • SQL과 유사하지만 테이블이 아닌 엔티티에 대해 쿼리를 작성한다.
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface PostRepository extends JpaRepository<Post, Long> {

    @Query("SELECT p FROM Post p WHERE p.title LIKE %:keyword%")
    List<Post> findByTitleContainingCustom(@Param("keyword") String keyword);

    // 다른 사용자 정의 쿼리 메서드들...
}
  • SELECT p : 엔티티 객체인 Post를 선택합니다.
  • FROM Post p : Post 엔터티를 대상으로 쿼리를 실행합니다. (p : Post의 별칭)
  • WHERE p.title LIKE %:keyword%: title 속성이 특정 키워드를 포함하는 경우를 선택합니다.

JPQL은 엔티티 객체를 기반으로 작성되기 때문에 테이블과 컬럼의 이름이 아닌 엔터티 클래스와 그 속성명을 사용합니다. 이를 통해 데이터베이스 종속성을 줄일 수 있습니다.

 

💡 만약 네이티브 SQL을 사용하고 싶다면.. nativeQuery 속성을 true로 설정

@Query(value = "SELECT * FROM post WHERE title LIKE %:keyword%", nativeQuery = true)
List<Post> findByTitleContainingNative(@Param("keyword") String keyword);

이렇게 하면 직접 SQL을 작성하여 원하는 쿼리를 실행할 수 있다.

다만 주의할 점은 네이티브 SQL을 사용할 때는 데이터베이스 종속성이 생길 수 있으므로(보안 문제 발생 가능성도 있음),

가능한 JPQL을 사용하여 데이터베이스에 독립적인 쿼리를 작성하는 것이 좋다.

 

3. QueryDSL(Domain Specific Language) : 

 

@Query 어노테이션은 명시적인 JPQL을 작성할 수 있게 해주지만, 쿼리가 복잡해지면 가독성이나 유지보수 측면에서 한계가 있을 수 있다. 이런 경우에 QueryDSL을 사용하면 좀 더 강력하고 유연한 쿼리를 작성할 수 있다.
QueryDSL은 JPQL을 자바 코드로 작성할 수 있도록 도와주는 라이브러리로, 코드 자체에서 쿼리를 구성할 수 있게 해준다. 이를 통해 컴파일 시점에서 타입 안정성을 확보하고, 복잡한 쿼리를 더 직관적으로 작성할 수 있다.

 

💡  QueryDSL 예시 코드를 보며 전체적인 흐름을 확인해보자.

 

1) 엔티티 클래스 (Post) :

엔티티 클래스는 데이터베이스의 테이블을 나타내는데 사용된다.

QueryDSL은 이 엔티티 클래스를 기반으로 Query 타입을 생성한다.

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    // Getter, Setter, 기타 메서드...
}

 

2) QueryDSL Query 타입 (QPost):

QPost 클래스는 QueryDSL을 사용하여 JPA 엔티티인 Post에 대한 타입 안전한(Query 타입) 표현을 정의한 클래스이다.

이 클래스를 사용하면 코드에서 엔티티의 속성에 대한 오타나 오류를 컴파일 시간에 확인할 수 있다.

import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.StringPath;
import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.core.types.dsl.Expressions;

public class QPost extends EntityPathBase<Post> {
    public final StringPath title = createString("title");
    public final NumberPath<Long> id = createNumber("id", Long.class);

    public QPost(String variable) {
        super(Post.class, variable);
    }
}
  • QPost extends EntityPathBase<Post> : EntityPathBase는 QueryDSL에서 사용하는 기본 클래스로, 엔티티에 대한 QueryDSL Query 타입을 생성하기 위한 메타 모델 클래스를 정의하는 데 사용된다.
  • public final StringPath title = createString("title"); : 이 부분은 title 필드에 대한 QueryDSL Query 타입을 정의한다. StringPath는 문자열에 대한 QueryDSL 타입을 나타내며, createString("title")은 title이라는 필드에 대한 QueryDSL 표현식을 생성한다.
  • public final NumberPath<Long> id = createNumber("id", Long.class); : 이 부분은 id 필드에 대한 QueryDSL Query 타입을 정의한다. NumberPath<Long>는 숫자(Long)에 대한 QueryDSL 타입을 나타내며, createNumber("id", Long.class)은 id라는 필드에 대한 QueryDSL 표현식을 생성한다.
  • public QPost(String variable) { super(Post.class, variable); } : 생성자는 QPost 클래스의 인스턴스를 초기화한다. Post.class는 이 QueryDSL Query 타입이 어떤 엔티티를 대상으로 하는지를 나타내며, variable은 QueryDSL에서 사용될 변수명을 지정한다.

이 클래스를 사용하면 QueryDSL을 활용하여 동적으로 쿼리를 작성할 수 있다.

예를 들어, QPost.post.title.eq("제목")와 같은 형태로 사용하여 title 필드가 "제목"과 같은지를 나타내는 조건을 만들 수 있다. QueryDSL을 사용하면 타입 안정성(type safety)을 확보하면서 동적 쿼리를 생성할 수 있다.

 

3) Repository 인터페이스 (PostRepository):

PostRepository 인터페이스는 Spring Data JPA의 CrudRepository를 확장하고, QueryDSL을 사용하여 동적인 쿼리를 처리하기 위한 QuerydslPredicateExecutor를 구현한다. (Spring Data JPA의 기본 기능 정의)

import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;

public interface PostRepository extends CrudRepository<Post, Long>, QuerydslPredicateExecutor<Post> {
    // 다른 메서드들...

    // QueryDSL을 사용한 동적인 쿼리
    List<Post> findAll(Predicate predicate);
    
    // Spring Data JPA가 제공하는 메서드 활용
    List<Post> findByTitle(String title);
}

 

4) Custom Repository 인터페이스 (CustomPostRepository) :

QueryDSL을 사용한 동적 쿼리를 직접 구현하는 인터페이스이다.

사용자가 정의한 쿼리 메서드를 추가한 인터페이스.

import java.util.List;

public interface CustomPostRepository {
    // QueryDSL을 사용한 동적인 쿼리
    List<Post> findPostsByTitle(String keyword);
}

 

5) Custom Repository 구현 클래스 (PostRepositoryImpl):

CustomPostRepository 인터페이스에서 정의된 메서드를 구현하는 클래스.

여기에서 QueryDSL을 사용하여 동적인 쿼리를 작성하고 실행한다.

import com.querydsl.jpa.impl.JPAQueryFactory;

import javax.persistence.EntityManager;

public class PostRepositoryImpl implements CustomPostRepository {

    private final JPAQueryFactory queryFactory;
    private final QPost qPost = QPost.post;

    public PostRepositoryImpl(EntityManager entityManager) {
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    @Override
    public List<Post> findPostsByTitle(String keyword) {
        return queryFactory.selectFrom(qPost)
                .where(qPost.title.like("%" + keyword + "%"))
                .fetch();
    }
}

 

 

✏️ 정리

이 외에  Named Query, Criteria API 방법도 존재한다.

여러 가지 방법을 적절히 조합하여 사용하는 것이 중요하며, 간단한 쿼리에는 메서드 이름 규칙을, 복잡한 쿼리에는 @Query 어노테이션을 또는 QueryDSL을 활용하는 것이 좋다.

+ Recent posts