공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.
이번 글에서는 SNS 서비스에 알림 기능을 추가하고, 이를 웹 페이지와 API를 통해 실제로 테스트해보는 과정을 다룬다. 알림 기능은 사용자가 작성한 게시글에 다른 사용자가 댓글을 달거나, 좋아요를 눌렀을 때 해당 게시글 작성자에게 알림을 주는 중요한 요소이다. 이를 통해 사용자는 자신의 게시글에 대한 피드백을 실시간으로 확인할 수 있어, 더 나은 사용자 경험을 제공할 수 있다.
Controller 관련 클래스
UserController 수정
먼저, UserController에 알림을 조회하는 메서드를 추가한다. 이 메서드는 인증된 사용자가 자신의 알림 목록을 페이징 처리된 형태로 조회할 수 있도록 구현된다.
@GetMapping("/alarm")
public Response<Page<AlarmResponse>> alarm(Pageable pageable, Authentication authentication) {
return Response.success(userService.alarmList(authentication.getName(), pageable).map(AlarmResponse::fromAlarm));
}
- 위 코드는 /alarm 경로로 GET 요청을 보내면, 인증된 사용자의 알림 목록을 반환하는 API다. 이때 Authentication 객체에서 인증된 사용자의 이름을 가져와 userService에 전달하고, 페이징 처리된 알림 리스트를 AlarmResponse 형태로 변환하여 응답한다.
UserControllerTest 수정
테스트 코드에서도 알림 기능이 정상적으로 작동하는지 확인한다. 수정 전 테스트 코드는 비로그인 사용자가 알림 리스트를 요청했을 때도 200 OK 상태 코드를 반환하는 문제가 있었다. 이를 수정하여, 비로그인 사용자는 401 Unauthorized 상태 코드를 받도록 변경하였다.
@Test
@WithAnonymousUser
void 알람리스트요청시_로그인하지_않은경우() throws Exception {
when(userService.alarmList(any(), any())).thenReturn(Page.empty());
mockMvc.perform(get("/api/v1/users/alarm")
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
//수정
.andExpect(status().isUnauthorized());
}
- 이제 비로그인 사용자가 알림을 조회하려고 할 때 401 Unauthorized 응답을 받는지 확인할 수 있다. 이를 통해 보안적인 문제도 해결되었다.
Service 관련 클래스
UserService 수정
다음으로, UserService에 알림 기능을 추가하여, 사용자가 자신과 관련된 알림을 조회할 수 있도록 구현한다.
private final AlarmEntityRepository alarmEntityRepository;
...
public Page<Alarm> alarmList(String userName, Pageable pageable) {
UserEntity userEntity = userEntityRepository.findByUserName(userName)
.orElseThrow(() -> new SnsApplicationException(ErrorCode.USER_NOT_FOUND,
String.format("%s not founded", userName)));
return alarmEntityRepository.findAllByUser(userEntity, pageable).map(Alarm::fromEntity);
}
- alarmList 메서드는 사용자의 알림을 조회하는 기능을 제공한다. 먼저 UserEntity를 조회하여 사용자가 존재하는지 확인한 후, 해당 사용자의 알림 목록을 AlarmEntityRepository를 통해 가져오고, 이를 Alarm 객체로 변환하여 반환한다.
PostService 수정
PostService는 게시물과 관련된 사용자의 활동에 따른 알림을 생성하는 로직을 포함하도록 수정되었다. 이제 사용자가 게시물에 좋아요를 누르거나 댓글을 작성할 경우, 해당 게시물의 작성자에게 알림이 자동으로 생성된다.
private final AlarmEntityRepository alarmEntityRepository;
...
@Transactional
public void like(Integer postId, String userName) {
PostEntity postEntity = getPostEntityOrException(postId);
UserEntity userEntity = getUserEntityOrException(userName);
likeEntityRepository.findByUserAndPost(userEntity, postEntity).ifPresent(it -> {
throw new SnsApplicationException(ErrorCode.ALREADY_LIKED,
String.format("userName %s already like post %d", userName, postId));
});
likeEntityRepository.save(LikeEntity.of(userEntity, postEntity));
// 알람 추가
alarmEntityRepository.save(AlarmEntity.of(postEntity.getUser(), AlarmType.NEW_LIKE_ON_POST,
new AlarmArgs(userEntity.getId(), postEntity.getId())));
}
- 게시물 및 사용자 유효성 확인
- getPostEntityOrException(postId) 메서드를 통해 해당 postId의 게시물이 실제로 존재하는지 확인하고, 만약 존재하지 않으면 예외를 던진다.
- getUserEntityOrException(userName) 메서드를 통해 주어진 userName에 해당하는 사용자가 있는지 확인하고, 없을 경우 예외가 발생한다.
- 좋아요 중복 확인
- likeEntityRepository.findByUserAndPost(userEntity, postEntity) 메서드를 통해 사용자가 이미 해당 게시물에 대해 좋아요를 눌렀는지 확인한다.
- 만약 이미 좋아요를 눌렀다면 ErrorCode.ALREADY_LIKED 예외를 던진다.
- 좋아요 저장
- 중복 좋아요가 아닌 경우, LikeEntity.of(userEntity, postEntity)를 생성하고 likeEntityRepository.save() 메서드를 사용해 새로운 좋아요 데이터를 저장한다.
- 알림 저장
- 좋아요 저장 후, alarmEntityRepository.save() 메서드를 통해 게시물의 작성자에게 알림을 추가한다. 이 알림은 AlarmType.NEW_LIKE_ON_POST 유형을 가지며, 알림의 AlarmArgs 객체는 좋아요를 누른 사용자와 해당 게시물의 ID를 포함한다.
- AlarmArgs(userEntity.getId(), postEntity.getId())는 알림에 포함될 구체적인 정보를 담는다. 즉, 누가 좋아요를 눌렀고 어떤 게시물에 좋아요를 눌렀는지를 알림에 저장하는 것이다.
@Transactional
public void comment(Integer postId, String userName, String comment) {
PostEntity postEntity = getPostEntityOrException(postId);
UserEntity userEntity = getUserEntityOrException(userName);
commentEntityRepository.save(CommentEntity.of(userEntity, postEntity, comment));
// 알람 추가
alarmEntityRepository.save(AlarmEntity.of(postEntity.getUser(), AlarmType.NEW_COMMENT_ON_POST,
new AlarmArgs(userEntity.getId(), postEntity.getId())));
}
- 게시물 및 사용자 유효성 확인
- getPostEntityOrException(postId)와 getUserEntityOrException(userName)를 통해 게시물과 사용자 정보를 검증한다. 게시물이나 사용자가 존재하지 않으면 각각 예외가 발생한다.
- 댓글 저장
- CommentEntity.of(userEntity, postEntity, comment) 메서드를 통해 새로운 댓글 엔티티를 생성하고, 이를 commentEntityRepository.save()로 데이터베이스에 저장한다.
- 알림 저장
- 댓글이 저장된 후, 게시물 작성자에게 새로운 댓글 알림을 생성한다. 이 때, AlarmType.NEW_COMMENT_ON_POST 알림 유형이 사용되며, 댓글 작성자의 ID와 댓글이 달린 게시물의 ID를 알림에 포함한다.
- AlarmArgs(userEntity.getId(), postEntity.getId())는 알림의 인자로 전달되며, 누가 댓글을 달았고 어떤 게시물에 댓글이 작성되었는지의 정보를 포함한다.
Entity 관련 클래스
먼저 build.gradle 파일에 다음과 같이 hibernate-types 라이브러리를 추가한다. 이 라이브러리는 JSON 타입을 처리하기 위해 사용된다.
dependencies {
// Hibernate types for handling JSON data
implementation 'com.vladmihalcea:hibernate-types-52:2.17.3'
}
- 이 의존성은 AlarmEntity 클래스에서 JsonBinaryType을 사용하여 JSON 형식의 데이터를 처리하는데 필요하다. 이를 통해 알림 시스템의 유연한 데이터 처리를 지원할 수 있다.
AlarmEntity 클래스
AlarmEntity 클래스는 알림 기능을 담당하는 핵심 엔티티로, 누가 어떤 알림을 받았는지를 저장한다. 다양한 애노테이션을 사용하여 알림을 효율적으로 관리할 수 있도록 설계되었으며, args 필드를 JSON 형식으로 저장해 유연한 데이터 구조를 제공한다. 이를 통해 다양한 알림 정보를 쉽게 관리할 수 있으며, AlarmEntityRepository를 통해 사용자별로 알림 데이터를 효율적으로 조회할 수 있다.
package com.example.sns.model.entity;
import com.example.sns.model.AlarmArgs;
import com.example.sns.model.AlarmType;
import com.vladmihalcea.hibernate.type.json.JsonBinaryType;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.Where;
import javax.persistence.*;
import java.sql.Timestamp;
import java.time.Instant;
@Entity
@Table(name = "\"alarm\"", indexes = {
@Index(name = "user_id_idx", columnList = "user_id")
})
@Getter
@Setter
@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
@SQLDelete(sql = "UPDATE \"alarm\" SET deleted_at = NOW() where id=?")
@Where(clause = "deleted_at is NULL")
@NoArgsConstructor
public class AlarmEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// 알람을 받은 사람
@ManyToOne
@JoinColumn(name = "user_id")
private UserEntity user;
@Enumerated(EnumType.STRING)
private AlarmType alarmType;
@Type(type = "jsonb")
@Column(columnDefinition = "json")
private AlarmArgs args;
@Column(name = "registered_at")
private Timestamp registeredAt;
@Column(name = "updated_at")
private Timestamp updatedAt;
@Column(name = "deleted_at")
private Timestamp deletedAt;
@PrePersist
void registeredAt() {
this.registeredAt = Timestamp.from(Instant.now());
}
@PreUpdate
void updatedAt() {
this.updatedAt = Timestamp.from(Instant.now());
}
public static AlarmEntity of(UserEntity userEntity, AlarmType alarmType, AlarmArgs args) {
AlarmEntity entity = new AlarmEntity();
entity.setUser(userEntity);
entity.setAlarmType(alarmType);
entity.setArgs(args);
return entity;
}
}
- 엔티티의 정의 및 테이블 설정
- @Entity 애노테이션을 통해 AlarmEntity가 데이터베이스 테이블과 매핑된다.
- @Table(name = "\"alarm\"", indexes = { @Index(name = "user_id_idx", columnList = "user_id") }) 설정을 통해 'alarm' 테이블에 'user_id' 인덱스를 생성하여 알림 조회 성능을 향상시킨다.
- 알람 관련 필드 정의
- @ManyToOne과 @JoinColumn을 사용해 알림을 받는 사용자(UserEntity)와의 관계를 정의한다.
- @Enumerated(EnumType.STRING)를 사용해 알림 유형(AlarmType)을 문자열로 저장한다. AlarmType은 좋아요, 댓글 등 알림의 구체적인 종류를 정의하는 Enum이다.
- @Type(type = "jsonb")를 사용해 알림에 필요한 추가 인자(AlarmArgs)를 JSON 형식으로 저장한다. 이로써 유연하게 다양한 정보(예: 누가 좋아요를 눌렀는지, 어떤 게시물에 댓글을 남겼는지)를 저장할 수 있다.
- 삭제 및 업데이트 처리
- @SQLDelete와 @Where 애노테이션을 사용하여 소프트 삭제를 구현했다. 삭제 시 실제로 데이터베이스에서 삭제하는 것이 아니라 deleted_at 필드에 삭제 시간을 기록하고, 조회 시 이 값이 NULL인 데이터만 조회하게 된다.
- 등록 및 수정 시간 관리
- @PrePersist와 @PreUpdate 메서드를 통해 등록 시간과 수정 시간을 자동으로 기록한다. 데이터가 처음 등록되거나 수정될 때, 각각 현재 시간을 저장한다.
- 정적 팩토리 메서드
- of(UserEntity userEntity, AlarmType alarmType, AlarmArgs args) 정적 메서드를 사용해 쉽게 AlarmEntity 객체를 생성할 수 있다. 이 메서드는 알림을 받는 사용자, 알림 유형, 알림과 관련된 인자들을 받아 AlarmEntity 객체를 반환한다.
AlarmEntityRepository 클래스
AlarmEntityRepository는 알림 데이터를 조회하고 관리하기 위한 인터페이스로, Spring Data JPA의 JpaRepository를 상속받아 기본적인 CRUD 기능을 제공한다.
package com.example.sns.repository;
import com.example.sns.model.entity.AlarmEntity;
import com.example.sns.model.entity.LikeEntity;
import com.example.sns.model.entity.PostEntity;
import com.example.sns.model.entity.UserEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface AlarmEntityRepository extends JpaRepository<AlarmEntity, Integer> {
Page<AlarmEntity> findAllByUser(UserEntity user, Pageable pageable);
}
유저별 알림 조회
- Page<AlarmEntity> findAllByUser(UserEntity user, Pageable pageable) 메서드를 통해 특정 사용자가 받은 모든 알림을 페이징 처리하여 조회할 수 있다.
- 이 메서드를 통해 알림 목록을 사용자에게 보여줄 때 효율적으로 데이터를 불러올 수 있다.
DTO 및 기타 모델 클래스
Alarm 클래스
이 클래스는 알림 엔티티를 기반으로 데이터를 전달하는 DTO이다. fromEntity 메서드를 통해 AlarmEntity를 Alarm 객체로 변환하여, 알림 정보를 다른 계층으로 전달한다. 알림 발생 시간과 삭제 여부 등의 상태 정보도 포함하고 있다.
package com.example.sns.model;
import com.example.sns.model.entity.AlarmEntity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.sql.Timestamp;
@Getter
@AllArgsConstructor
public class Alarm {
private Integer id;
private User user;
private AlarmType alarmType;
private AlarmArgs args;
private Timestamp registeredAt;
private Timestamp updatedAt;
private Timestamp deletedAt;
public static Alarm fromEntity(AlarmEntity entity) {
return new Alarm(
entity.getId(),
User.fromEntity(entity.getUser()),
entity.getAlarmType(),
entity.getArgs(),
entity.getRegisteredAt(),
entity.getUpdatedAt(),
entity.getDeletedAt()
);
}
}
AlarmArgs 클래스
알림을 발생시킨 사용자와 그 대상이 되는 객체(예: 게시물)를 저장하는 클래스이다. 알림이 발생할 때, 누가 어떤 대상에 대해 알림을 발생시켰는지의 정보를 관리한다.
package com.example.sns.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class AlarmArgs {
// 알람을 발생시킨 사람
private Integer fromUserId;
private Integer targetId;
}
AlarmType 클래스
알림의 유형을 정의한 열거형(enum) 클래스이다. 새로운 댓글이나 좋아요 발생 시 각각 다른 메시지 텍스트를 설정하여 알림 유형에 따른 메시지를 처리할 수 있도록 한다.
package com.example.sns.model;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum AlarmType {
NEW_COMMENT_ON_POST("new comment!"),
NEW_LIKE_ON_POST("new like!"),
;
private final String alarmText;
}
AlarmResponse 클래스
클라이언트로 알림 정보를 전달하기 위한 DTO 클래스이다. Alarm 객체를 받아 이를 AlarmResponse로 변환하며, 사용자가 볼 알림 텍스트와 함께 알림의 상세 정보를 제공한다.
package com.example.sns.controller.response;
import com.example.sns.model.*;
import com.example.sns.model.entity.UserEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import java.sql.Timestamp;
@Data
@AllArgsConstructor
public class AlarmResponse {
private Integer id;
private AlarmType alarmType;
private AlarmArgs alarmArgs;
private String text;
private Timestamp registeredAt;
private Timestamp updatedAt;
private Timestamp deletedAt;
public static AlarmResponse fromAlarm(Alarm alarm) {
return new AlarmResponse(
alarm.getId(),
alarm.getAlarmType(),
alarm.getArgs(),
alarm.getAlarmType().getAlarmText(),
alarm.getRegisteredAt(),
alarm.getUpdatedAt(),
alarm.getDeletedAt()
);
}
}
6. 실행 테스트
1) 포스트맨으로 API 테스트
1-1) 게시글 댓글 작성
게시글 1번에 댓글을 달아본다. 댓글 작성 요청을 보내면 댓글이 저장되고, 동시에 게시물 작성자에게 알람이 저장된다.
1-2) 알람 조회
이제 알람 테이블을 조회하여 알람이 정상적으로 저장되었는지 확인한다. 알람에는 어떤 유저가 어떤 게시글에 대해 좋아요를 눌렀는지, 또는 댓글을 달았는지에 대한 정보가 포함된다.
- 알람 1개가 정상적으로 등록되었다.
2) 실제 웹 실행 테스트
2-1) 게시글 확인
이제 실제 웹으로 띄워서 확인해 본다. admin 계정으로 로그인 후 Feed로 가서 test04 유저가 쓴 게시글을 눌러본다.
현재 받은 좋아요 개수와 댓글들을 확인할 수 있다.
댓글은 한 페이지에서 5개까지 보이며, 5개 이상이 되면 페이지네이션이 적용되어 페이지를 넘겨가며 댓글을 볼 수 있다.
2-2) 좋아요 확인
test05가 쓴 게시글에 좋아요를 눌러본다.
정상적으로 좋아요 카운팅이 증가하며, 알람 또한 생성된다.
2-3) 알람 기능 확인
이제 admin 계정으로 글을 작성하고, admin1 계정으로 댓글과 좋아요를 눌러본 후 다시 admin 계정으로 로그인하여 알람을 확인해 본다.
알람 리스트 페이지에서 새로운 댓글과 좋아요에 대한 알람을 확인할 수 있다.
결과적으로 모든 테스트가 정상적으로 통과하며, 알림 기능이 웹 상에서도 정상적으로 작동하는 것을 확인할 수 있다.
'BackEnd > Project' 카테고리의 다른 글
[SNS] Ch04. 코드 최적화 (0) | 2024.09.10 |
---|---|
[SNS] Ch04. 대규모 트래픽 시 문제점 분석 및 해결 (0) | 2024.09.10 |
[SNS] Ch03. 알림 기능 테스트 작성 (0) | 2024.09.09 |
[SNS] Ch03. 댓글 기능 개발 (0) | 2024.09.09 |
[SNS] Ch03. 댓글 기능 테스트 작성 (0) | 2024.09.09 |