Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/commerce-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")

// security
implementation("org.springframework.security:spring-security-crypto")

// querydsl
annotationProcessor("com.querydsl:querydsl-apt::jakarta")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
annotationProcessor("jakarta.annotation:jakarta.annotation-api")

// test
testRuntimeOnly("com.h2database:h2")

// test-fixtures
testImplementation(testFixtures(project(":modules:jpa")))
testImplementation(testFixtures(project(":modules:redis")))
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.loopers.application.user;

import com.loopers.domain.user.User;
import com.loopers.domain.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.LocalDate;

@Component
@RequiredArgsConstructor
public class UserFacade {

private final UserService userService;

public UserInfo register(String loginId, String password, String name, LocalDate birthDate, String email) {
User user = userService.register(loginId, password, name, birthDate, email);
return UserInfo.from(user);
}

public UserInfo getUserInfo(String loginId, String password) {
User user = userService.getUserInfo(loginId, password);
return UserInfo.from(user);
}

public void updatePassword(String loginId, String currentPassword, String newPassword, LocalDate birthDate) {
userService.updatePassword(loginId, currentPassword, newPassword, birthDate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.loopers.application.user;

import com.loopers.domain.user.User;

import java.time.LocalDate;

public record UserInfo(
Long id,
String loginId,
String name,
LocalDate birthDate,
String email
) {
public static UserInfo from(User user) {
return new UserInfo(
user.getId(),
user.getLoginId(),
user.getName(),
user.getBirthDate(),
user.getEmail()
);
}

public String getMaskedName() {
if (name == null || name.isEmpty()) {
return name;
}
if (name.length() == 1) {
return "*";
}
return name.substring(0, name.length() - 1) + "*";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.loopers.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfig {

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

This file was deleted.

This file was deleted.

This file was deleted.

78 changes: 78 additions & 0 deletions apps/commerce-api/src/main/java/com/loopers/domain/user/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.loopers.domain.user;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import lombok.Getter;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_login_id", columnList = "login_id"),
@Index(name = "idx_email", columnList = "email")
})
@Getter
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;

@Column(name = "login_id", length = 50, nullable = false, unique = true)
private String loginId;

@Column(name = "password", length = 255, nullable = false)
private String password;

@Column(name = "name", length = 100, nullable = false)
private String name;

@Column(name = "birth_date", nullable = false)
private LocalDate birthDate;

@Column(name = "email", length = 255, nullable = false)
private String email;

@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;

protected User() {}

public static User create(String loginId, String password, String name, LocalDate birthDate, String email) {
User user = new User();
user.loginId = loginId;
user.password = password;
user.name = name;
user.birthDate = birthDate;
user.email = email;
return user;
}

@PrePersist
private void prePersist() {
LocalDateTime now = LocalDateTime.now();
this.createdAt = now;
this.updatedAt = now;
}

@PreUpdate
private void preUpdate() {
this.updatedAt = LocalDateTime.now();
}

public void updatePassword(String newPassword) {
this.password = newPassword;
}
}
Comment on lines 17 to 78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/User.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 2752


🏁 Script executed:

# Find all `@Entity` classes in the domain package
fd -e java --path '*/domain/**' . --exec grep -l '@Entity' {} \;

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 318


🏁 Script executed:

# Check for equals/hashCode implementations in domain entities
rg -n "public boolean equals|public int hashCode" --type java -g '**/domain/**/*.java' -B2

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

# Find all Java files in domain package
find apps/commerce-api/src/main/java -path '*/domain/**' -name '*.java' -type f

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 299


🏁 Script executed:

# Check for `@EqualsAndHashCode` Lombok annotation usage
rg -n "@EqualsAndHashCode" --type java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

# Check if User entity is used in collections or as cache keys
rg -n "Set<User|Map.*User|HashSet.*User|HashMap.*User" --type java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 366


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 5026


🏁 Script executed:

# Check for any L2 cache configuration or EntityManager.merge() usage
rg -n "merge|cache|Cache|CACHE" --type java apps/commerce-api/src/main/java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


JPA 엔티티 식별자 기반 equals/hashCode 구현 추가 권장한다.

User 엔티티는 현재 equals/hashCode가 구현되지 않았다. 현재 코드에서는 개별 엔티티 조회/저장 패턴만 사용하므로 즉각적인 운영 문제는 없으나, 향후 다음과 같은 상황에서 예상치 못한 동작이 발생할 수 있다:

  • 엔티티를 Set이나 Map에 저장하거나 컬렉션에서 검색할 경우
  • 캐싱 또는 second-level cache 도입 시
  • EntityManager.merge() 사용 시

데이터베이스 식별자 기반으로 equals/hashCode를 구현하면 장기적 운영 안정성을 확보할 수 있다:

`@Override`
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return id != null && id.equals(user.id);
}

`@Override`
public int hashCode() {
    return getClass().hashCode();
}

추가로 저장되지 않은 엔티티(id=null)와 저장된 엔티티 간 동치성 비교를 검증하는 단위 테스트를 작성한다.

🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/User.java` around
lines 17 - 78, Add JPA identifier-based equals/hashCode to User: implement
equals(Object) to return true for same instance, false for null/different class,
and compare id fields only when id != null (i.e., id.equals(other.id));
implement hashCode() to return getClass().hashCode(). Update the User class by
adding these two methods (referencing the User class and its id field) and
include a unit test verifying equality behavior between transient (id == null)
and persisted (id != null) instances.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.loopers.domain.user;

import java.util.Optional;

public interface UserRepository {
User save(User user);
Optional<User> findByLoginId(String loginId);
boolean existsByLoginId(String loginId);
}
Loading